1use std::error::Error;
4use std::fmt;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use riffdb_types::{
8 AgentSessionId, ApplicationExportOperationId, ApplicationInstallationCampaignId, CapabilityId,
9 ContractMigrationOperationId, OfflineMaintenanceOperationId, RequestId,
10 UuidV7ConstructionError,
11};
12
13const MAX_UUID_V7_UNIX_MILLISECONDS: u128 = 0xffff_ffff_ffff;
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum IdentifierGenerationError {
18 ClockBeforeUnixEpoch,
20 TimestampOutOfRange,
22 EntropyUnavailable,
24}
25
26impl fmt::Display for IdentifierGenerationError {
27 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28 formatter.write_str(match self {
29 Self::ClockBeforeUnixEpoch => "system clock is before the Unix epoch",
30 Self::TimestampOutOfRange => "system clock is outside the UUIDv7 range",
31 Self::EntropyUnavailable => "operating-system entropy is unavailable",
32 })
33 }
34}
35
36impl Error for IdentifierGenerationError {}
37
38#[derive(Clone, Copy, Debug, Default)]
43pub struct SystemIdSource;
44
45impl SystemIdSource {
46 #[must_use]
48 pub const fn new() -> Self {
49 Self
50 }
51
52 pub fn request_id(self) -> Result<RequestId, IdentifierGenerationError> {
54 generate_with(
55 SystemTime::now,
56 fill_system_random,
57 RequestId::from_unix_milliseconds_and_random,
58 )
59 }
60
61 pub fn capability_id(self) -> Result<CapabilityId, IdentifierGenerationError> {
63 generate_with(
64 SystemTime::now,
65 fill_system_random,
66 CapabilityId::from_unix_milliseconds_and_random,
67 )
68 }
69
70 pub fn agent_session_id(self) -> Result<AgentSessionId, IdentifierGenerationError> {
72 generate_with(
73 SystemTime::now,
74 fill_system_random,
75 AgentSessionId::from_unix_milliseconds_and_random,
76 )
77 }
78
79 pub fn offline_maintenance_operation_id(
81 self,
82 ) -> Result<OfflineMaintenanceOperationId, IdentifierGenerationError> {
83 generate_with(
84 SystemTime::now,
85 fill_system_random,
86 OfflineMaintenanceOperationId::from_unix_milliseconds_and_random,
87 )
88 }
89
90 pub fn contract_migration_operation_id(
92 self,
93 ) -> Result<ContractMigrationOperationId, IdentifierGenerationError> {
94 generate_with(
95 SystemTime::now,
96 fill_system_random,
97 ContractMigrationOperationId::from_unix_milliseconds_and_random,
98 )
99 }
100
101 pub fn application_export_operation_id(
103 self,
104 ) -> Result<ApplicationExportOperationId, IdentifierGenerationError> {
105 generate_with(
106 SystemTime::now,
107 fill_system_random,
108 ApplicationExportOperationId::from_unix_milliseconds_and_random,
109 )
110 }
111
112 pub fn application_installation_campaign_id(
114 self,
115 ) -> Result<ApplicationInstallationCampaignId, IdentifierGenerationError> {
116 generate_with(
117 SystemTime::now,
118 fill_system_random,
119 ApplicationInstallationCampaignId::from_unix_milliseconds_and_random,
120 )
121 }
122}
123
124pub fn generate_request_id() -> Result<RequestId, IdentifierGenerationError> {
126 SystemIdSource::new().request_id()
127}
128
129pub fn generate_capability_id() -> Result<CapabilityId, IdentifierGenerationError> {
131 SystemIdSource::new().capability_id()
132}
133
134pub fn generate_agent_session_id() -> Result<AgentSessionId, IdentifierGenerationError> {
136 SystemIdSource::new().agent_session_id()
137}
138
139pub fn generate_offline_maintenance_operation_id()
141-> Result<OfflineMaintenanceOperationId, IdentifierGenerationError> {
142 SystemIdSource::new().offline_maintenance_operation_id()
143}
144
145pub fn generate_contract_migration_operation_id()
147-> Result<ContractMigrationOperationId, IdentifierGenerationError> {
148 SystemIdSource::new().contract_migration_operation_id()
149}
150
151pub fn generate_application_export_operation_id()
153-> Result<ApplicationExportOperationId, IdentifierGenerationError> {
154 SystemIdSource::new().application_export_operation_id()
155}
156
157pub fn generate_application_installation_campaign_id()
159-> Result<ApplicationInstallationCampaignId, IdentifierGenerationError> {
160 SystemIdSource::new().application_installation_campaign_id()
161}
162
163const SYSTEM_RANDOM_CHUNK_COUNT: usize = 25;
164const SYSTEM_RANDOM_BUFFER_BYTES: usize = SYSTEM_RANDOM_CHUNK_COUNT * 10;
165
166struct ThreadRandomBuffer {
167 bytes: [u8; SYSTEM_RANDOM_BUFFER_BYTES],
168 offset: usize,
169}
170
171impl Default for ThreadRandomBuffer {
172 fn default() -> Self {
173 Self {
174 bytes: [0_u8; SYSTEM_RANDOM_BUFFER_BYTES],
175 offset: SYSTEM_RANDOM_BUFFER_BYTES,
176 }
177 }
178}
179
180std::thread_local! {
181 static THREAD_RANDOM_BUFFER: std::cell::RefCell<ThreadRandomBuffer> =
182 std::cell::RefCell::new(ThreadRandomBuffer::default());
183}
184
185fn fill_system_random(output: &mut [u8; 10]) -> Result<(), IdentifierGenerationError> {
186 THREAD_RANDOM_BUFFER.with(|cell| {
187 let Ok(mut buf) = cell.try_borrow_mut() else {
188 return getrandom::fill(output)
189 .map_err(|_| IdentifierGenerationError::EntropyUnavailable);
190 };
191 if buf.offset + 10 > SYSTEM_RANDOM_BUFFER_BYTES {
192 if getrandom::fill(&mut buf.bytes).is_err() {
193 buf.offset = SYSTEM_RANDOM_BUFFER_BYTES;
194 return Err(IdentifierGenerationError::EntropyUnavailable);
195 }
196 buf.offset = 0;
197 }
198 let start = buf.offset;
199 let end = start + 10;
200 output.copy_from_slice(&buf.bytes[start..end]);
201 buf.bytes[start..end].fill(0);
202 buf.offset = end;
203 Ok(())
204 })
205}
206
207fn generate_with<T>(
208 now: impl FnOnce() -> SystemTime,
209 fill: impl FnOnce(&mut [u8; 10]) -> Result<(), IdentifierGenerationError>,
210 construct: fn(u64, [u8; 10]) -> Result<T, UuidV7ConstructionError>,
211) -> Result<T, IdentifierGenerationError> {
212 let elapsed = now()
213 .duration_since(UNIX_EPOCH)
214 .map_err(|_| IdentifierGenerationError::ClockBeforeUnixEpoch)?;
215 let milliseconds = elapsed.as_millis();
216 if milliseconds > MAX_UUID_V7_UNIX_MILLISECONDS {
217 return Err(IdentifierGenerationError::TimestampOutOfRange);
218 }
219 let milliseconds =
220 u64::try_from(milliseconds).map_err(|_| IdentifierGenerationError::TimestampOutOfRange)?;
221
222 let mut random = [0_u8; 10];
223 fill(&mut random)?;
224 construct(milliseconds, random).map_err(|_| IdentifierGenerationError::TimestampOutOfRange)
225}
226
227#[cfg(test)]
228mod tests {
229 use std::sync::Mutex;
230 use std::sync::atomic::{AtomicUsize, Ordering};
231 use std::time::Duration;
232
233 use super::*;
234
235 #[test]
236 fn one_candidate_samples_clock_then_fills_exactly_ten_bytes_once() {
237 let calls = Mutex::new(Vec::new());
238 let clock_calls = AtomicUsize::new(0);
239 let entropy_calls = AtomicUsize::new(0);
240
241 let request_id = generate_with(
242 || {
243 clock_calls.fetch_add(1, Ordering::SeqCst);
244 calls.lock().expect("call log").push("clock");
245 UNIX_EPOCH + Duration::from_millis(0x0123_4567_89ab)
246 },
247 |output| {
248 entropy_calls.fetch_add(1, Ordering::SeqCst);
249 calls.lock().expect("call log").push("entropy");
250 assert_eq!(output.len(), 10);
251 output.copy_from_slice(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
252 Ok(())
253 },
254 RequestId::from_unix_milliseconds_and_random,
255 )
256 .expect("candidate");
257
258 assert_eq!(clock_calls.load(Ordering::SeqCst), 1);
259 assert_eq!(entropy_calls.load(Ordering::SeqCst), 1);
260 assert_eq!(*calls.lock().expect("call log"), ["clock", "entropy"]);
261 assert_eq!(
262 request_id.to_string(),
263 "01234567-89ab-7001-8203-040506070809"
264 );
265 }
266
267 #[test]
268 fn invalid_clock_never_requests_entropy() {
269 let entropy_calls = AtomicUsize::new(0);
270 let result = generate_with(
271 || {
272 UNIX_EPOCH
273 .checked_sub(Duration::from_millis(1))
274 .expect("time")
275 },
276 |_| {
277 entropy_calls.fetch_add(1, Ordering::SeqCst);
278 Ok(())
279 },
280 CapabilityId::from_unix_milliseconds_and_random,
281 );
282 assert_eq!(result, Err(IdentifierGenerationError::ClockBeforeUnixEpoch));
283 assert_eq!(entropy_calls.load(Ordering::SeqCst), 0);
284 }
285
286 #[test]
287 fn entropy_failure_returns_no_partial_identifier_and_does_not_retry() {
288 let entropy_calls = AtomicUsize::new(0);
289 let result = generate_with(
290 || UNIX_EPOCH,
291 |_| {
292 entropy_calls.fetch_add(1, Ordering::SeqCst);
293 Err(IdentifierGenerationError::EntropyUnavailable)
294 },
295 AgentSessionId::from_unix_milliseconds_and_random,
296 );
297 assert_eq!(result, Err(IdentifierGenerationError::EntropyUnavailable));
298 assert_eq!(entropy_calls.load(Ordering::SeqCst), 1);
299 }
300
301 #[test]
302 fn timestamp_above_uuid_v7_range_never_requests_entropy() {
303 let entropy_calls = AtomicUsize::new(0);
304 let duration = Duration::from_millis(0x1_0000_0000_0000);
305 let result = generate_with(
306 || UNIX_EPOCH + duration,
307 |_| {
308 entropy_calls.fetch_add(1, Ordering::SeqCst);
309 Ok(())
310 },
311 RequestId::from_unix_milliseconds_and_random,
312 );
313 assert_eq!(result, Err(IdentifierGenerationError::TimestampOutOfRange));
314 assert_eq!(entropy_calls.load(Ordering::SeqCst), 0);
315 }
316}