use std::fmt;
use std::num::NonZeroU64;
use serde::{Deserialize, Deserializer, Serialize};
const OPAQUE_BYTES: usize = 16;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct ExecutionId([u8; OPAQUE_BYTES]);
impl ExecutionId {
pub const LEN: usize = OPAQUE_BYTES * 2;
pub fn mint() -> Self {
ExecutionId(random_bytes())
}
pub fn parse(value: &str) -> Result<Self, InvalidIdentity> {
parse_hex(value).map(ExecutionId)
}
pub fn as_key_segment(&self) -> String {
format!("x{self}")
}
}
impl fmt::Display for ExecutionId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write_hex(&self.0, formatter)
}
}
impl fmt::Debug for ExecutionId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "ExecutionId({self})")
}
}
impl Serialize for ExecutionId {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for ExecutionId {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = String::deserialize(deserializer)?;
ExecutionId::parse(&value).map_err(serde::de::Error::custom)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct ProducerId([u8; OPAQUE_BYTES]);
impl ProducerId {
pub fn mint() -> Self {
ProducerId(random_bytes())
}
pub fn parse(value: &str) -> Result<Self, InvalidIdentity> {
parse_hex(value).map(ProducerId)
}
}
impl fmt::Display for ProducerId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write_hex(&self.0, formatter)
}
}
impl fmt::Debug for ProducerId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "ProducerId({self})")
}
}
impl Serialize for ProducerId {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_bytes(&self.0)
}
}
impl<'de> Deserialize<'de> for ProducerId {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let bytes = serde_bytes::ByteBuf::deserialize(deserializer)?;
<[u8; OPAQUE_BYTES]>::try_from(bytes.as_ref())
.map(ProducerId)
.map_err(|_| {
serde::de::Error::custom(format!(
"producer id must be {OPAQUE_BYTES} bytes, got {}",
bytes.len()
))
})
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct TimelineId(NonZeroU64);
impl TimelineId {
pub fn mint() -> Self {
let mut bytes = [0_u8; 8];
getrandom::fill(&mut bytes).expect("the host must provide randomness");
TimelineId(NonZeroU64::new(u64::from_le_bytes(bytes)).unwrap_or(NonZeroU64::MIN))
}
pub const fn from_raw(value: u64) -> Option<Self> {
match NonZeroU64::new(value) {
Some(value) => Some(TimelineId(value)),
None => None,
}
}
pub const fn get(self) -> u64 {
self.0.get()
}
}
impl fmt::Display for TimelineId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "t{:016x}", self.0.get())
}
}
impl fmt::Debug for TimelineId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "TimelineId({self})")
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{0}")]
pub struct InvalidIdentity(String);
fn random_bytes() -> [u8; OPAQUE_BYTES] {
let mut bytes = [0_u8; OPAQUE_BYTES];
getrandom::fill(&mut bytes).expect("the host must provide randomness");
bytes
}
fn write_hex(bytes: &[u8; OPAQUE_BYTES], formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in bytes {
write!(formatter, "{byte:02x}")?;
}
Ok(())
}
fn parse_hex(value: &str) -> Result<[u8; OPAQUE_BYTES], InvalidIdentity> {
let value = value.strip_prefix('x').unwrap_or(value);
if value.len() != OPAQUE_BYTES * 2 {
return Err(InvalidIdentity(format!(
"expected {} hex characters, got {}",
OPAQUE_BYTES * 2,
value.len()
)));
}
let mut bytes = [0_u8; OPAQUE_BYTES];
for (index, byte) in bytes.iter_mut().enumerate() {
let pair = &value[index * 2..index * 2 + 2];
*byte = u8::from_str_radix(pair, 16)
.map_err(|_| InvalidIdentity(format!("'{pair}' is not a hex byte")))?;
}
Ok(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn execution_ids_are_opaque_unique_and_key_safe() {
let first = ExecutionId::mint();
let second = ExecutionId::mint();
assert_ne!(first, second);
let segment = first.as_key_segment();
assert_eq!(segment.len(), ExecutionId::LEN + 1);
assert!(segment.starts_with('x'));
assert!(!segment.contains('/') && !segment.contains('*'));
assert_eq!(ExecutionId::parse(&segment), Ok(first));
assert_eq!(ExecutionId::parse(&first.to_string()), Ok(first));
}
#[test]
fn a_malformed_execution_id_is_rejected_rather_than_truncated() {
assert!(ExecutionId::parse("").is_err());
assert!(ExecutionId::parse("xdeadbeef").is_err());
assert!(ExecutionId::parse(&"z".repeat(ExecutionId::LEN)).is_err());
}
#[test]
fn producer_ids_round_trip_through_the_wire_encoding() {
let producer = ProducerId::mint();
let encoded = rmp_serde::to_vec_named(&producer).unwrap();
let decoded: ProducerId = rmp_serde::from_slice(&encoded).unwrap();
assert_eq!(decoded, producer);
assert_ne!(producer, ProducerId::mint());
}
#[test]
fn timelines_have_no_zero_value_and_no_generation_order() {
assert_eq!(TimelineId::from_raw(0), None);
let timeline = TimelineId::mint();
assert_eq!(TimelineId::from_raw(timeline.get()), Some(timeline));
assert_ne!(timeline, TimelineId::mint());
}
}