1use std::fmt;
26use std::num::NonZeroU64;
27
28use serde::{Deserialize, Deserializer, Serialize};
29
30fn is_participant_token(value: &str) -> bool {
38 !value.is_empty()
39 && value.chars().all(|character| {
40 character.is_ascii_lowercase()
41 || character.is_ascii_digit()
42 || matches!(character, '_' | '-')
43 })
44}
45
46#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
53pub struct ParticipantId(String);
54
55impl ParticipantId {
56 pub fn new(value: impl Into<String>) -> Result<Self, ParticipantIdError> {
58 let value = value.into();
59 if is_participant_token(&value) {
60 Ok(Self(value))
61 } else {
62 Err(ParticipantIdError(value))
63 }
64 }
65
66 #[must_use]
68 pub fn as_str(&self) -> &str {
69 &self.0
70 }
71}
72
73impl std::fmt::Display for ParticipantId {
74 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 formatter.write_str(&self.0)
76 }
77}
78
79impl AsRef<str> for ParticipantId {
80 fn as_ref(&self) -> &str {
81 self.as_str()
82 }
83}
84
85impl std::str::FromStr for ParticipantId {
86 type Err = ParticipantIdError;
87
88 fn from_str(value: &str) -> Result<Self, Self::Err> {
89 Self::new(value)
90 }
91}
92
93impl TryFrom<String> for ParticipantId {
94 type Error = ParticipantIdError;
95
96 fn try_from(value: String) -> Result<Self, Self::Error> {
97 Self::new(value)
98 }
99}
100
101impl From<ParticipantId> for String {
102 fn from(value: ParticipantId) -> Self {
103 value.0
104 }
105}
106
107impl Serialize for ParticipantId {
108 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
109 serializer.serialize_str(self.as_str())
110 }
111}
112
113impl<'de> Deserialize<'de> for ParticipantId {
114 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
115 Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
116 }
117}
118
119#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
121#[error("participant id must be a non-empty lowercase token, got '{0}'")]
122pub struct ParticipantIdError(String);
123
124#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
130pub struct ParticipantArtifactId(String);
131
132impl ParticipantArtifactId {
133 pub fn new(value: impl Into<String>) -> Result<Self, ParticipantArtifactIdError> {
135 let value = value.into();
136 if is_participant_token(&value) {
137 Ok(Self(value))
138 } else {
139 Err(ParticipantArtifactIdError(value))
140 }
141 }
142
143 #[must_use]
145 pub fn as_str(&self) -> &str {
146 &self.0
147 }
148}
149
150impl fmt::Display for ParticipantArtifactId {
151 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
152 formatter.write_str(self.as_str())
153 }
154}
155
156impl AsRef<str> for ParticipantArtifactId {
157 fn as_ref(&self) -> &str {
158 self.as_str()
159 }
160}
161
162impl std::str::FromStr for ParticipantArtifactId {
163 type Err = ParticipantArtifactIdError;
164
165 fn from_str(value: &str) -> Result<Self, Self::Err> {
166 Self::new(value)
167 }
168}
169
170impl TryFrom<String> for ParticipantArtifactId {
171 type Error = ParticipantArtifactIdError;
172
173 fn try_from(value: String) -> Result<Self, Self::Error> {
174 Self::new(value)
175 }
176}
177
178impl From<ParticipantArtifactId> for String {
179 fn from(value: ParticipantArtifactId) -> Self {
180 value.0
181 }
182}
183
184impl Serialize for ParticipantArtifactId {
185 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
186 serializer.serialize_str(self.as_str())
187 }
188}
189
190impl<'de> Deserialize<'de> for ParticipantArtifactId {
191 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
192 Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
193 }
194}
195
196#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
198#[error("participant artifact id must be a non-empty lowercase token, got '{0}'")]
199pub struct ParticipantArtifactIdError(String);
200
201const ZID_BYTES: usize = 16;
203
204const ZID_HEX_LEN: usize = ZID_BYTES * 2;
206
207const CANONICAL_TOP_NIBBLE: u128 = 1 << 124;
213
214fn mint_canonical_value() -> u128 {
220 let mut bytes = [0_u8; ZID_BYTES];
221 #[expect(
222 clippy::expect_used,
223 reason = "a session identity is the root of bus provenance; a host without randomness cannot safely start one"
224 )]
225 getrandom::fill(&mut bytes).expect("the host must provide randomness");
226 let mut value = u128::from_be_bytes(bytes);
227 if value >> 124 == 0 {
228 value |= CANONICAL_TOP_NIBBLE;
229 }
230 value
231}
232
233fn canonical_hex(value: u128) -> String {
234 format!("{value:032x}")
235}
236
237#[derive(Clone, Copy, PartialEq, Eq, Hash)]
249pub struct ExecutionId(u128);
250
251impl ExecutionId {
252 pub const LEN: usize = ZID_HEX_LEN;
254
255 pub fn mint() -> Self {
264 ExecutionId(mint_canonical_value())
265 }
266
267 pub fn parse(value: &str) -> Result<Self, IdentityError> {
276 if value.len() != ZID_HEX_LEN || !value.bytes().all(is_lowercase_hex) {
277 return Err(IdentityError(format!(
278 "an execution id is exactly {ZID_HEX_LEN} lowercase hexadecimal \
279 characters, got '{value}'"
280 )));
281 }
282 let value = u128::from_str_radix(value, 16)
283 .map_err(|error| IdentityError(format!("'{value}' is not hexadecimal: {error}")))?;
284 ExecutionId::try_from(value)
285 }
286}
287
288impl TryFrom<u128> for ExecutionId {
289 type Error = IdentityError;
290
291 fn try_from(value: u128) -> Result<Self, IdentityError> {
292 if value >> 124 == 0 {
293 return Err(IdentityError(format!(
294 "an execution id renders as {ZID_HEX_LEN} characters, so its most \
295 significant nibble is never zero"
296 )));
297 }
298 Ok(ExecutionId(value))
299 }
300}
301
302impl From<ExecutionId> for u128 {
303 fn from(execution: ExecutionId) -> Self {
304 execution.0
305 }
306}
307
308impl fmt::Display for ExecutionId {
309 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
310 formatter.write_str(&canonical_hex(self.0))
311 }
312}
313
314impl fmt::Debug for ExecutionId {
315 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
316 write!(formatter, "ExecutionId({self})")
317 }
318}
319
320impl Serialize for ExecutionId {
321 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
322 serializer.serialize_str(&self.to_string())
323 }
324}
325
326impl<'de> Deserialize<'de> for ExecutionId {
327 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
328 let value = String::deserialize(deserializer)?;
329 ExecutionId::parse(&value).map_err(serde::de::Error::custom)
330 }
331}
332
333#[derive(Clone, Copy, PartialEq, Eq, Hash)]
342pub struct ProducerId(u128);
343
344impl ProducerId {
345 pub const LEN: usize = ZID_HEX_LEN;
347
348 pub fn parse(value: &str) -> Result<Self, IdentityError> {
353 if value.len() != ZID_HEX_LEN || !value.bytes().all(is_lowercase_hex) {
354 return Err(IdentityError(format!(
355 "a producer id is exactly {ZID_HEX_LEN} lowercase hexadecimal characters \
356 and is not zero, got '{value}'"
357 )));
358 }
359 let value = u128::from_str_radix(value, 16)
360 .map_err(|error| IdentityError(format!("'{value}' is not hexadecimal: {error}")))?;
361 ProducerId::try_from(value)
362 }
363}
364
365impl TryFrom<u128> for ProducerId {
366 type Error = IdentityError;
367
368 fn try_from(value: u128) -> Result<Self, IdentityError> {
369 if value >> 124 == 0 {
370 return Err(IdentityError(
371 "a producer id must have a non-zero leading nibble".to_string(),
372 ));
373 }
374 Ok(ProducerId(value))
375 }
376}
377
378impl From<ProducerId> for u128 {
379 fn from(producer: ProducerId) -> Self {
380 producer.0
381 }
382}
383
384impl fmt::Display for ProducerId {
385 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
386 formatter.write_str(&canonical_hex(self.0))
387 }
388}
389
390impl fmt::Debug for ProducerId {
391 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
392 write!(formatter, "ProducerId({self})")
393 }
394}
395
396impl Serialize for ProducerId {
397 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
398 serializer.serialize_bytes(&self.0.to_le_bytes())
402 }
403}
404
405impl<'de> Deserialize<'de> for ProducerId {
406 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
407 let bytes = serde_bytes::ByteBuf::deserialize(deserializer)?;
408 let bytes = <[u8; ZID_BYTES]>::try_from(bytes.as_ref()).map_err(|_| {
409 serde::de::Error::custom(format!(
410 "producer id must be {ZID_BYTES} bytes, got {}",
411 bytes.len()
412 ))
413 })?;
414 ProducerId::try_from(u128::from_le_bytes(bytes)).map_err(serde::de::Error::custom)
415 }
416}
417
418#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
425#[serde(transparent)]
426pub struct TimelineId(NonZeroU64);
427
428impl TimelineId {
429 pub fn mint() -> Self {
431 let mut bytes = [0_u8; 8];
432 #[expect(
433 clippy::expect_used,
434 reason = "a timeline names one world history, so two histories separated by a \
435 predictable identity would be indistinguishable to every reader; a host \
436 whose randomness source is unavailable has no correct value to return"
437 )]
438 getrandom::fill(&mut bytes).expect("the host must provide randomness");
439 TimelineId(NonZeroU64::new(u64::from_le_bytes(bytes)).unwrap_or(NonZeroU64::MIN))
442 }
443
444 pub const fn from_raw(value: u64) -> Option<Self> {
446 match NonZeroU64::new(value) {
447 Some(value) => Some(TimelineId(value)),
448 None => None,
449 }
450 }
451
452 pub const fn get(self) -> u64 {
454 self.0.get()
455 }
456}
457
458impl fmt::Display for TimelineId {
459 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
460 write!(formatter, "t{:016x}", self.0.get())
461 }
462}
463
464impl fmt::Debug for TimelineId {
465 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
466 write!(formatter, "TimelineId({self})")
467 }
468}
469
470#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
477#[error("{0}")]
478pub struct IdentityError(String);
479
480const fn is_lowercase_hex(byte: u8) -> bool {
481 byte.is_ascii_digit() || byte.is_ascii_lowercase() && byte <= b'f'
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487
488 #[test]
489 fn participant_ids_are_typed_canonical_tokens() {
490 let id = ParticipantId::new("front_camera").expect("a canonical participant id");
491 assert_eq!(id.as_str(), "front_camera");
492 assert_eq!(id.to_string(), "front_camera");
493 assert_eq!(
494 serde_json::to_string(&id).expect("id serializes"),
495 "\"front_camera\""
496 );
497 assert_eq!(
498 serde_json::from_str::<ParticipantId>("\"front_camera\"").expect("id deserializes"),
499 id
500 );
501 }
502
503 #[test]
504 fn participant_ids_reject_noncanonical_and_path_tokens() {
505 for value in ["", "FrontCamera", "front camera", "../brain", "brain/extra"] {
506 assert!(ParticipantId::new(value).is_err(), "{value:?}");
507 assert!(
508 serde_json::from_str::<ParticipantId>(&format!("\"{value}\"")).is_err(),
509 "{value:?}"
510 );
511 }
512 }
513
514 #[test]
515 fn a_minted_execution_always_renders_at_the_canonical_width() {
516 let first = ExecutionId::mint();
517 let second = ExecutionId::mint();
518 assert_ne!(first, second);
519
520 let rendered = first.to_string();
521 assert_eq!(rendered.len(), ExecutionId::LEN);
522 assert!(!rendered.starts_with('0'));
523 assert!(rendered.bytes().all(is_lowercase_hex));
524 assert!(!rendered.contains('/') && !rendered.contains('*'));
525 assert_eq!(ExecutionId::parse(&rendered), Ok(first));
526 }
527
528 #[test]
529 fn minting_does_not_pin_the_leading_digit_to_half_the_alphabet() {
530 let saw_even_leading_digit = (0..64).any(|_| {
534 let leading = ExecutionId::mint().to_string().as_bytes()[0];
535 let digit = if leading.is_ascii_digit() {
536 leading - b'0'
537 } else {
538 leading - b'a' + 10
539 };
540 digit % 2 == 0
541 });
542 assert!(
543 saw_even_leading_digit,
544 "a minted execution covers the whole nonzero leading-digit range"
545 );
546 }
547
548 #[test]
549 fn only_the_canonical_execution_form_parses() {
550 let canonical = ExecutionId::mint().to_string();
551
552 assert!(ExecutionId::parse("").is_err());
553 assert!(ExecutionId::parse("deadbeef").is_err());
554 assert!(
555 ExecutionId::parse(&canonical.to_uppercase()).is_err(),
556 "uppercase renders back differently, so it is not the same identity"
557 );
558 assert!(
559 ExecutionId::parse(&format!("0{}", &canonical[1..])).is_err(),
560 "a leading zero would render back one character shorter"
561 );
562 assert!(
563 ExecutionId::parse(&format!("{canonical}0")).is_err(),
564 "an over-long run of digits is not a session identity"
565 );
566 assert!(ExecutionId::parse(&"z".repeat(ExecutionId::LEN)).is_err());
567 assert!(
568 ExecutionId::parse(&format!("x{canonical}")).is_err(),
569 "the key root is bare, so there is no prefix to strip"
570 );
571 }
572
573 #[test]
574 fn an_execution_round_trips_through_its_session_identity_value() {
575 let execution = ExecutionId::mint();
576 let value = u128::from(execution);
577 assert_eq!(ExecutionId::try_from(value), Ok(execution));
578 assert_eq!(format!("{value:x}"), execution.to_string());
579 assert!(
580 ExecutionId::try_from(u128::from(execution) >> 4).is_err(),
581 "a value that renders narrower than the canonical width is not an execution"
582 );
583 assert!(ExecutionId::try_from(0).is_err());
584 }
585
586 #[test]
587 fn a_producer_round_trips_in_the_canonical_transport_form() {
588 let minted = ProducerId::try_from((1_u128 << 124) | 0x0123_4567_89ab_cdef).unwrap();
589 assert_eq!(minted.to_string().len(), ProducerId::LEN);
590 assert_eq!(ProducerId::parse(&minted.to_string()), Ok(minted));
591
592 let wide = ProducerId::try_from(u128::MAX).unwrap();
593 assert_eq!(wide.to_string(), "f".repeat(ZID_HEX_LEN));
594 assert_eq!(ProducerId::parse(&wide.to_string()), Ok(wide));
595
596 assert!(ProducerId::try_from(0).is_err());
597 assert!(ProducerId::parse("").is_err());
598 assert!(ProducerId::parse("01").is_err());
599 assert!(ProducerId::parse("AB").is_err());
600 assert!(ProducerId::parse(&"f".repeat(ZID_HEX_LEN + 1)).is_err());
601 assert!(ProducerId::parse(&format!("0{}", "f".repeat(ZID_HEX_LEN - 1))).is_err());
602 }
603
604 #[test]
605 fn producer_ids_round_trip_through_the_wire_encoding() {
606 let producer = ProducerId::try_from((1_u128 << 124) | 0x0123_4567_89ab_cdef).unwrap();
607 let encoded = rmp_serde::to_vec_named(&producer).unwrap();
608 let decoded: ProducerId = rmp_serde::from_slice(&encoded).unwrap();
609 assert_eq!(decoded, producer);
610 assert_ne!(producer, ProducerId::try_from((1_u128 << 124) | 1).unwrap());
611 }
612
613 #[test]
614 fn timelines_have_no_zero_value_and_no_generation_order() {
615 assert_eq!(TimelineId::from_raw(0), None);
616 let timeline = TimelineId::mint();
617 assert_eq!(TimelineId::from_raw(timeline.get()), Some(timeline));
618 assert_ne!(timeline, TimelineId::mint());
622 }
623}