Skip to main content

kacrab_protocol/
uuid.rs

1//! [`KafkaUuid`] — 128-bit UUID with Kafka-specific encoding.
2//!
3//! Wraps [`uuid::Uuid`] and adds:
4//!
5//! * Base64 URL-safe (no padding) string format — matches the Java client and topic-id wire format.
6//! * Reserved constants ([`KafkaUuid::ZERO`], [`KafkaUuid::ONE`],
7//!   [`KafkaUuid::METADATA_TOPIC_ID`]).
8//! * [`KafkaUuid::random`] that avoids reserved values and base64 reps that start with `-`.
9
10pub mod error;
11
12use std::{cmp::Ordering, fmt};
13
14use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
15use bytes::{Buf, Bytes, BytesMut};
16
17pub use self::error::UuidError;
18use crate::primitives::check_remaining;
19
20/// Result alias for UUID parse operations.
21pub type Result<T> = core::result::Result<T, UuidError>;
22
23/// 128-bit UUID with Kafka-specific encoding semantics.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub struct KafkaUuid(uuid::Uuid);
26
27impl KafkaUuid {
28    /// Size in bytes (128 bits).
29    pub const SIZE: usize = 16;
30
31    /// Maximum length of a base64 representation (16 bytes → 22 chars + slack).
32    pub const MAX_BASE64_LEN: usize = 24;
33
34    /// Maximum retries for [`Self::random`] before giving up.
35    pub const MAX_RANDOM_RETRIES: u32 = 64;
36
37    /// The nil UUID (all zeros).
38    pub const ZERO: Self = Self(uuid::Uuid::nil());
39
40    /// UUID with value 1. Reserved — never returned by [`Self::random`].
41    pub const ONE: Self = Self(uuid::Uuid::from_u128(1));
42
43    /// Metadata topic ID in `KRaft` mode (alias for [`Self::ONE`]).
44    pub const METADATA_TOPIC_ID: Self = Self::ONE;
45
46    /// Wrap a raw [`uuid::Uuid`].
47    #[must_use]
48    pub const fn from_uuid(inner: uuid::Uuid) -> Self {
49        Self(inner)
50    }
51
52    /// Construct from two 64-bit halves (most-significant first).
53    #[must_use]
54    pub const fn from_parts(msb: u64, lsb: u64) -> Self {
55        Self(uuid::Uuid::from_u64_pair(msb, lsb))
56    }
57
58    /// Generate a random non-reserved type-4 UUID whose base64 form does not
59    /// start with `-`. Returns [`UuidError::RandomExhausted`] if no candidate
60    /// is found within [`Self::MAX_RANDOM_RETRIES`] attempts.
61    pub fn random() -> Result<Self> {
62        for _ in 0..Self::MAX_RANDOM_RETRIES {
63            let candidate = Self(uuid::Uuid::new_v4());
64            if candidate.is_reserved() {
65                continue;
66            }
67            let encoded = URL_SAFE_NO_PAD.encode(candidate.0.as_bytes());
68            if !encoded.starts_with('-') {
69                return Ok(candidate);
70            }
71        }
72        Err(UuidError::RandomExhausted {
73            retries: Self::MAX_RANDOM_RETRIES,
74        })
75    }
76
77    /// Borrow the inner [`uuid::Uuid`].
78    #[must_use]
79    pub const fn inner(&self) -> &uuid::Uuid {
80        &self.0
81    }
82
83    /// Most-significant 64 bits.
84    #[must_use]
85    pub const fn most_significant_bits(self) -> u64 {
86        self.0.as_u64_pair().0
87    }
88
89    /// Least-significant 64 bits.
90    #[must_use]
91    pub const fn least_significant_bits(self) -> u64 {
92        self.0.as_u64_pair().1
93    }
94
95    /// `true` for [`Self::ZERO`] and [`Self::ONE`].
96    #[must_use]
97    pub const fn is_reserved(self) -> bool {
98        let (msb, lsb) = self.0.as_u64_pair();
99        msb == 0 && (lsb == 0 || lsb == 1)
100    }
101
102    /// `true` if this is the nil (all-zero) UUID.
103    #[must_use]
104    pub const fn is_nil(self) -> bool {
105        self.0.is_nil()
106    }
107
108    /// Raw 16-byte representation.
109    #[must_use]
110    pub const fn to_bytes(self) -> [u8; 16] {
111        *self.0.as_bytes()
112    }
113
114    /// Parse from a base64 URL-safe string (no padding).
115    pub fn from_base64(s: &str) -> Result<Self> {
116        if s.len() > Self::MAX_BASE64_LEN {
117            return Err(UuidError::StringTooLong {
118                length: s.len(),
119                max: Self::MAX_BASE64_LEN,
120            });
121        }
122        let decoded = URL_SAFE_NO_PAD
123            .decode(s)
124            .map_err(|e| UuidError::InvalidBase64 {
125                message: e.to_string(),
126            })?;
127        if decoded.len() != Self::SIZE {
128            return Err(UuidError::InvalidLength {
129                expected: Self::SIZE,
130                actual: decoded.len(),
131            });
132        }
133        let arr: [u8; 16] = decoded.try_into().map_err(|_| UuidError::InvalidLength {
134            expected: Self::SIZE,
135            actual: 0,
136        })?;
137        Ok(Self(uuid::Uuid::from_bytes(arr)))
138    }
139}
140
141impl Default for KafkaUuid {
142    fn default() -> Self {
143        Self::ZERO
144    }
145}
146
147impl fmt::Display for KafkaUuid {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        f.write_str(&URL_SAFE_NO_PAD.encode(self.0.as_bytes()))
150    }
151}
152
153impl PartialOrd for KafkaUuid {
154    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
155        Some(self.cmp(other))
156    }
157}
158
159impl Ord for KafkaUuid {
160    fn cmp(&self, other: &Self) -> Ordering {
161        let (self_high, self_low) = self.0.as_u64_pair();
162        let (other_high, other_low) = other.0.as_u64_pair();
163        self_high.cmp(&other_high).then(self_low.cmp(&other_low))
164    }
165}
166
167impl From<uuid::Uuid> for KafkaUuid {
168    fn from(u: uuid::Uuid) -> Self {
169        Self(u)
170    }
171}
172
173impl From<KafkaUuid> for uuid::Uuid {
174    fn from(k: KafkaUuid) -> Self {
175        k.0
176    }
177}
178
179// ---------------------------------------------------------------------------
180// Wire-level read/write — UUID is fixed 16 bytes, no length prefix.
181// ---------------------------------------------------------------------------
182
183/// Read a UUID (16 bytes, raw).
184pub fn read_uuid(buf: &mut Bytes) -> crate::error::Result<KafkaUuid> {
185    check_remaining(buf, KafkaUuid::SIZE)?;
186    let mut arr = [0u8; 16];
187    buf.copy_to_slice(&mut arr);
188    Ok(KafkaUuid(uuid::Uuid::from_bytes(arr)))
189}
190
191/// Write a UUID (16 bytes, raw).
192#[expect(
193    clippy::trivially_copy_pass_by_ref,
194    reason = "Generated protocol encoders pass borrowed struct fields and array elements; by-ref \
195              keeps the wire helper signature uniform with non-Copy field writers."
196)]
197pub fn write_uuid(buf: &mut BytesMut, value: &KafkaUuid) {
198    buf.extend_from_slice(&value.to_bytes());
199}