1pub 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
20pub type Result<T> = core::result::Result<T, UuidError>;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub struct KafkaUuid(uuid::Uuid);
26
27impl KafkaUuid {
28 pub const SIZE: usize = 16;
30
31 pub const MAX_BASE64_LEN: usize = 24;
33
34 pub const MAX_RANDOM_RETRIES: u32 = 64;
36
37 pub const ZERO: Self = Self(uuid::Uuid::nil());
39
40 pub const ONE: Self = Self(uuid::Uuid::from_u128(1));
42
43 pub const METADATA_TOPIC_ID: Self = Self::ONE;
45
46 #[must_use]
48 pub const fn from_uuid(inner: uuid::Uuid) -> Self {
49 Self(inner)
50 }
51
52 #[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 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 #[must_use]
79 pub const fn inner(&self) -> &uuid::Uuid {
80 &self.0
81 }
82
83 #[must_use]
85 pub const fn most_significant_bits(self) -> u64 {
86 self.0.as_u64_pair().0
87 }
88
89 #[must_use]
91 pub const fn least_significant_bits(self) -> u64 {
92 self.0.as_u64_pair().1
93 }
94
95 #[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 #[must_use]
104 pub const fn is_nil(self) -> bool {
105 self.0.is_nil()
106 }
107
108 #[must_use]
110 pub const fn to_bytes(self) -> [u8; 16] {
111 *self.0.as_bytes()
112 }
113
114 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
179pub 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#[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}