use anyhow::{Result, anyhow};
use multibase::Base;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
macro_rules! define_id_newtype {
($(#[$meta:meta])* $name:ident, $label:literal) => {
$(#[$meta])*
#[derive(Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
pub struct $name(u64);
impl $name {
pub fn new(id: u64) -> Self {
Self(id)
}
pub fn as_u64(&self) -> u64 {
self.0
}
pub const INVALID: $name = $name(u64::MAX);
pub fn is_invalid(&self) -> bool {
self.0 == u64::MAX
}
pub const EPHEMERAL_BIT: u64 = 1u64 << 63;
pub fn ephemeral(transient_id: u64) -> Self {
if transient_id >= Self::EPHEMERAL_BIT {
return Self::INVALID;
}
Self(Self::EPHEMERAL_BIT | transient_id)
}
pub fn is_ephemeral(&self) -> bool {
self.0 & Self::EPHEMERAL_BIT != 0 && !self.is_invalid()
}
pub fn transient_id(&self) -> Option<u64> {
self.is_ephemeral().then_some(self.0 & !Self::EPHEMERAL_BIT)
}
}
impl From<u64> for $name {
fn from(val: u64) -> Self {
Self(val)
}
}
impl From<$name> for u64 {
fn from(id: $name) -> Self {
id.0
}
}
impl Default for $name {
fn default() -> Self {
Self::INVALID
}
}
impl fmt::Debug for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_invalid() {
write!(f, concat!($label, "(INVALID)"))
} else {
write!(f, concat!($label, "({})"), self.0)
}
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for $name {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
let id: u64 = s
.parse()
.map_err(|e| anyhow!(concat!("Invalid ", $label, " '{}': {}"), s, e))?;
Ok(Self::new(id))
}
}
};
}
define_id_newtype!(
Vid,
"Vid"
);
define_id_newtype!(
Eid,
"Eid"
);
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct DenseIdx(pub u32);
impl DenseIdx {
pub fn new(idx: u32) -> Self {
Self(idx)
}
pub fn as_usize(&self) -> usize {
self.0 as usize
}
pub fn as_u32(&self) -> u32 {
self.0
}
pub const INVALID: DenseIdx = DenseIdx(u32::MAX);
pub fn is_invalid(&self) -> bool {
self.0 == u32::MAX
}
}
impl From<u32> for DenseIdx {
fn from(val: u32) -> Self {
Self(val)
}
}
impl From<usize> for DenseIdx {
fn from(val: usize) -> Self {
Self(val as u32)
}
}
impl fmt::Display for DenseIdx {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct UniId([u8; 32]);
impl UniId {
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub fn from_multibase(s: &str) -> Result<Self> {
let (base, bytes) =
multibase::decode(s).map_err(|e| anyhow!("Multibase decode error: {}", e))?;
if base != Base::Base32Lower {
return Err(anyhow!(
"UniId must use Base32Lower encoding, got {:?}",
base
));
}
let inner: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
anyhow!("Invalid UniId length: expected 32 bytes, got {}", v.len())
})?;
Ok(Self(inner))
}
pub fn to_multibase(&self) -> String {
multibase::encode(Base::Base32Lower, self.0)
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl fmt::Debug for UniId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "UniId({})", self.to_multibase())
}
}
impl fmt::Display for UniId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_multibase())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vid_basic() {
let vid = Vid::new(12345);
assert_eq!(vid.as_u64(), 12345);
assert!(!vid.is_invalid());
}
#[test]
fn test_vid_invalid() {
let vid = Vid::INVALID;
assert!(vid.is_invalid());
assert_eq!(vid.as_u64(), u64::MAX);
}
#[test]
fn test_vid_from_str() {
let vid: Vid = "42".parse().unwrap();
assert_eq!(vid.as_u64(), 42);
let original = Vid::new(12345678);
let s = original.to_string();
let parsed: Vid = s.parse().unwrap();
assert_eq!(original, parsed);
assert!("invalid".parse::<Vid>().is_err());
assert!("".parse::<Vid>().is_err());
}
#[test]
fn test_eid_basic() {
let eid = Eid::new(67890);
assert_eq!(eid.as_u64(), 67890);
assert!(!eid.is_invalid());
}
#[test]
fn test_eid_invalid() {
let eid = Eid::INVALID;
assert!(eid.is_invalid());
assert_eq!(eid.as_u64(), u64::MAX);
}
#[test]
fn test_eid_from_str() {
let eid: Eid = "100".parse().unwrap();
assert_eq!(eid.as_u64(), 100);
let original = Eid::new(0xABCDEF);
let s = original.to_string();
let parsed: Eid = s.parse().unwrap();
assert_eq!(original, parsed);
assert!("invalid".parse::<Eid>().is_err());
}
#[test]
fn test_dense_idx() {
let idx = DenseIdx::new(100);
assert_eq!(idx.as_usize(), 100);
assert_eq!(idx.as_u32(), 100);
assert!(!idx.is_invalid());
let invalid = DenseIdx::INVALID;
assert!(invalid.is_invalid());
}
#[test]
fn test_uni_id_multibase() {
let bytes = [0u8; 32];
let uid = UniId(bytes);
let s = uid.to_multibase();
let decoded = UniId::from_multibase(&s).unwrap();
assert_eq!(uid, decoded);
}
mod security_tests {
use super::*;
#[test]
fn test_uni_id_rejects_wrong_encoding() {
let bytes = [0u8; 32];
let base58_encoded = multibase::encode(multibase::Base::Base58Btc, bytes);
let result = UniId::from_multibase(&base58_encoded);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Base32Lower encoding")
);
}
#[test]
fn test_uni_id_rejects_wrong_length() {
let short_bytes = [0u8; 16];
let encoded = multibase::encode(Base::Base32Lower, short_bytes);
let result = UniId::from_multibase(&encoded);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("expected 32 bytes")
);
}
}
}