use rand::RngCore;
use rusqlite::ToSql;
use rusqlite::types::{ToSqlOutput, ValueRef};
use serde::{Deserialize, Serialize};
use std::fmt;
const BASE62_ALPHABET: &[u8; 62] =
b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
fn mint_base62<const N: usize>() -> String {
let mut rng = rand::rng();
let mut buf = [0u8; N];
for b in &mut buf {
*b = BASE62_ALPHABET[(rng.next_u32() % 62) as usize];
}
String::from_utf8(buf.to_vec()).unwrap()
}
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BoxID(String);
impl BoxID {
pub const FULL_LENGTH: usize = 12;
pub const LEGACY_LENGTH: usize = 26;
pub const SHORT_LENGTH: usize = 8;
pub fn parse(s: &str) -> Option<Self> {
if Self::is_valid(s) {
Some(Self(s.to_string()))
} else {
None
}
}
pub fn is_valid(s: &str) -> bool {
(s.len() == Self::FULL_LENGTH || s.len() == Self::LEGACY_LENGTH)
&& s.bytes().all(|b| b.is_ascii_alphanumeric())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn short(&self) -> &str {
&self.0[..Self::SHORT_LENGTH]
}
pub fn starts_with(&self, prefix: &str) -> bool {
self.0.starts_with(prefix)
}
}
impl fmt::Display for BoxID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl fmt::Debug for BoxID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "BoxID({})", self.short())
}
}
impl AsRef<str> for BoxID {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::borrow::Borrow<str> for BoxID {
fn borrow(&self) -> &str {
&self.0
}
}
impl ToSql for BoxID {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(ToSqlOutput::Borrowed(ValueRef::Text(self.0.as_bytes())))
}
}
pub struct BoxIDMint;
impl BoxIDMint {
pub fn mint() -> BoxID {
BoxID(mint_base62::<{ BoxID::FULL_LENGTH }>())
}
}
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BaseDiskID(String);
impl BaseDiskID {
pub const FULL_LENGTH: usize = 8;
pub const SHORT_LENGTH: usize = 8;
pub fn parse(s: &str) -> Option<Self> {
if Self::is_valid(s) {
Some(Self(s.to_string()))
} else {
None
}
}
pub fn is_valid(s: &str) -> bool {
s.len() == Self::FULL_LENGTH && s.bytes().all(|b| b.is_ascii_alphanumeric())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn short(&self) -> &str {
&self.0[..Self::SHORT_LENGTH]
}
pub fn starts_with(&self, prefix: &str) -> bool {
self.0.starts_with(prefix)
}
}
impl fmt::Display for BaseDiskID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl fmt::Debug for BaseDiskID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "BaseDiskID({})", self.short())
}
}
impl AsRef<str> for BaseDiskID {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::borrow::Borrow<str> for BaseDiskID {
fn borrow(&self) -> &str {
&self.0
}
}
impl ToSql for BaseDiskID {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(ToSqlOutput::Borrowed(ValueRef::Text(self.0.as_bytes())))
}
}
pub struct BaseDiskIDMint;
impl BaseDiskIDMint {
pub fn mint() -> BaseDiskID {
BaseDiskID(mint_base62::<{ BaseDiskID::FULL_LENGTH }>())
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use std::collections::HashSet;
#[test]
fn test_mint_length() {
let id = BoxIDMint::mint();
assert_eq!(id.as_str().len(), BoxID::FULL_LENGTH);
}
#[test]
fn test_mint_uniqueness() {
let ids: HashSet<String> = (0..1000)
.map(|_| BoxIDMint::mint().as_str().to_string())
.collect();
assert_eq!(ids.len(), 1000, "all 1000 minted IDs should be unique");
}
#[test]
fn test_mint_alphabet() {
for _ in 0..100 {
let id = BoxIDMint::mint();
for ch in id.as_str().chars() {
assert!(ch.is_ascii_alphanumeric(), "unexpected char: {ch}");
}
}
}
#[test]
fn test_mint_produces_valid_id() {
let id = BoxIDMint::mint();
assert_eq!(id.as_str().len(), BoxID::FULL_LENGTH);
assert!(BoxID::is_valid(id.as_str()));
}
#[test]
fn test_parse_valid() {
assert!(BoxID::parse("aB3cD4eF5gH6").is_some());
assert!(BoxID::parse("000000000000").is_some());
assert!(BoxID::parse("zzzzzzzzzzzz").is_some());
}
#[test]
fn test_parse_legacy_ulid() {
assert!(BoxID::parse("01HJK4TNRPQSXYZ8WM6NCVT9R1").is_some());
assert!(BoxID::parse("01234567890123456789012345").is_some());
}
#[test]
fn test_parse_invalid() {
assert!(BoxID::parse("abc").is_none(), "too short");
assert!(BoxID::parse("aB3cD4eF5gH6X").is_none(), "13 chars");
assert!(BoxID::parse("aB3cD4eF5g-!").is_none(), "non-alphanumeric");
assert!(
BoxID::parse("0123456789012345678901234").is_none(),
"25 chars"
);
}
#[test]
fn test_short() {
let id = BoxID::parse("aB3cD4eF5gH6").unwrap();
assert_eq!(id.short(), "aB3cD4eF");
assert_eq!(id.short().len(), BoxID::SHORT_LENGTH);
}
#[test]
fn test_display() {
let id = BoxID::parse("aB3cD4eF5gH6").unwrap();
assert_eq!(format!("{id}"), "aB3cD4eF5gH6");
}
#[test]
fn test_debug() {
let id = BoxID::parse("aB3cD4eF5gH6").unwrap();
let debug = format!("{id:?}");
assert_eq!(debug, "BoxID(aB3cD4eF)");
}
#[test]
fn test_starts_with() {
let id = BoxID::parse("aB3cD4eF5gH6").unwrap();
assert!(id.starts_with("aB3"));
assert!(!id.starts_with("xyz"));
}
#[test]
fn test_base_disk_mint_length() {
let id = BaseDiskIDMint::mint();
assert_eq!(id.as_str().len(), BaseDiskID::FULL_LENGTH);
}
#[test]
fn test_base_disk_mint_uniqueness() {
let ids: HashSet<String> = (0..1000)
.map(|_| BaseDiskIDMint::mint().as_str().to_string())
.collect();
assert_eq!(ids.len(), 1000, "all 1000 minted IDs should be unique");
}
#[test]
fn test_base_disk_mint_alphabet() {
for _ in 0..100 {
let id = BaseDiskIDMint::mint();
for ch in id.as_str().chars() {
assert!(ch.is_ascii_alphanumeric(), "unexpected char: {ch}");
}
}
}
#[test]
fn test_base_disk_mint_produces_valid_id() {
let id = BaseDiskIDMint::mint();
assert_eq!(id.as_str().len(), BaseDiskID::FULL_LENGTH);
assert!(BaseDiskID::is_valid(id.as_str()));
}
#[test]
fn test_base_disk_parse_valid() {
assert!(BaseDiskID::parse("aB3cD4eF").is_some());
assert!(BaseDiskID::parse("00000000").is_some());
assert!(BaseDiskID::parse("zzzzzzzz").is_some());
}
#[test]
fn test_base_disk_parse_invalid() {
assert!(BaseDiskID::parse("abc").is_none(), "too short");
assert!(BaseDiskID::parse("aB3cD4eF5").is_none(), "9 chars");
assert!(BaseDiskID::parse("aB3cD4-_").is_none(), "non-alphanumeric");
}
#[test]
fn test_base_disk_short() {
let id = BaseDiskID::parse("aB3cD4eF").unwrap();
assert_eq!(id.short(), "aB3cD4eF");
assert_eq!(id.short().len(), BaseDiskID::SHORT_LENGTH);
}
#[test]
fn test_base_disk_display() {
let id = BaseDiskID::parse("aB3cD4eF").unwrap();
assert_eq!(format!("{id}"), "aB3cD4eF");
}
#[test]
fn test_base_disk_debug() {
let id = BaseDiskID::parse("aB3cD4eF").unwrap();
let debug = format!("{id:?}");
assert_eq!(debug, "BaseDiskID(aB3cD4eF)");
}
#[test]
fn test_base_disk_starts_with() {
let id = BaseDiskID::parse("aB3cD4eF").unwrap();
assert!(id.starts_with("aB3"));
assert!(!id.starts_with("xyz"));
}
proptest! {
#[test]
fn prop_mint_always_valid(_seed in any::<u64>()) {
let id = BoxIDMint::mint();
prop_assert!(BoxID::is_valid(id.as_str()));
prop_assert_eq!(id.as_str().len(), BoxID::FULL_LENGTH);
}
#[test]
fn prop_parse_roundtrip_12(s in "[0-9A-Za-z]{12}") {
let id = BoxID::parse(&s).unwrap();
prop_assert_eq!(id.as_str(), s.as_str());
}
#[test]
fn prop_parse_roundtrip_26(s in "[0-9A-Za-z]{26}") {
let id = BoxID::parse(&s).unwrap();
prop_assert_eq!(id.as_str(), s.as_str());
}
#[test]
fn prop_parse_rejects_wrong_length(s in "[0-9A-Za-z]{1,50}") {
prop_assume!(s.len() != BoxID::FULL_LENGTH && s.len() != BoxID::LEGACY_LENGTH);
prop_assert!(BoxID::parse(&s).is_none());
}
#[test]
fn prop_parse_rejects_non_alphanumeric(s in ".{12}") {
prop_assume!(s.bytes().any(|b| !b.is_ascii_alphanumeric()));
prop_assert!(BoxID::parse(&s).is_none());
}
#[test]
fn prop_short_is_prefix_of_full(s in "[0-9A-Za-z]{12}") {
let id = BoxID::parse(&s).unwrap();
prop_assert!(id.as_str().starts_with(id.short()));
prop_assert_eq!(id.short().len(), BoxID::SHORT_LENGTH);
}
#[test]
fn prop_display_matches_as_str(s in "[0-9A-Za-z]{12}") {
let id = BoxID::parse(&s).unwrap();
prop_assert_eq!(format!("{id}"), id.as_str());
}
#[test]
fn prop_serde_roundtrip(s in "[0-9A-Za-z]{12}") {
let id = BoxID::parse(&s).unwrap();
let json = serde_json::to_string(&id).unwrap();
let back: BoxID = serde_json::from_str(&json).unwrap();
prop_assert_eq!(id, back);
}
#[test]
fn prop_base_disk_mint_always_valid(_seed in any::<u64>()) {
let id = BaseDiskIDMint::mint();
prop_assert!(BaseDiskID::is_valid(id.as_str()));
prop_assert_eq!(id.as_str().len(), BaseDiskID::FULL_LENGTH);
}
#[test]
fn prop_base_disk_parse_roundtrip(s in "[0-9A-Za-z]{8}") {
let id = BaseDiskID::parse(&s).unwrap();
prop_assert_eq!(id.as_str(), s.as_str());
}
#[test]
fn prop_base_disk_parse_rejects_wrong_length(s in "[0-9A-Za-z]{1,50}") {
prop_assume!(s.len() != BaseDiskID::FULL_LENGTH);
prop_assert!(BaseDiskID::parse(&s).is_none());
}
#[test]
fn prop_base_disk_parse_rejects_non_alphanumeric(s in ".{8}") {
prop_assume!(s.bytes().any(|b| !b.is_ascii_alphanumeric()));
prop_assert!(BaseDiskID::parse(&s).is_none());
}
#[test]
fn prop_base_disk_display_matches_as_str(s in "[0-9A-Za-z]{8}") {
let id = BaseDiskID::parse(&s).unwrap();
prop_assert_eq!(format!("{id}"), id.as_str());
}
#[test]
fn prop_base_disk_serde_roundtrip(s in "[0-9A-Za-z]{8}") {
let id = BaseDiskID::parse(&s).unwrap();
let json = serde_json::to_string(&id).unwrap();
let back: BaseDiskID = serde_json::from_str(&json).unwrap();
prop_assert_eq!(id, back);
}
}
}