cloud_sdk/retry/
idempotency.rs1use core::fmt;
4
5use cloud_sdk_sanitization::sanitize_bytes;
6use subtle::{Choice, ConstantTimeEq};
7
8use super::fingerprint::FingerprintRef;
9
10pub const MIN_IDEMPOTENCY_INTENT_BYTES: usize = 16;
12pub const MAX_IDEMPOTENCY_INTENT_BYTES: usize = 64;
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum IdempotencyIntentError {
18 TooShort,
20 TooLong,
22 AllZero,
24}
25
26impl_static_error!(IdempotencyIntentError,
27 Self::TooShort => "idempotency intent is too short",
28 Self::TooLong => "idempotency intent is too long",
29 Self::AllZero => "idempotency intent cannot be all zero",
30);
31
32pub struct IdempotencyIntent<'secret> {
47 bytes: &'secret mut [u8],
48}
49
50impl<'secret> IdempotencyIntent<'secret> {
51 pub fn new(source: &'secret mut [u8]) -> Result<Self, IdempotencyIntentError> {
53 if let Err(error) = validate_source(source) {
54 sanitize_bytes(source);
55 return Err(error);
56 }
57 Ok(Self { bytes: source })
58 }
59
60 #[must_use]
62 pub const fn len(&self) -> usize {
63 self.bytes.len()
64 }
65
66 #[must_use]
68 pub const fn is_empty(&self) -> bool {
69 false
70 }
71}
72
73impl fmt::Debug for IdempotencyIntent<'_> {
74 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75 formatter.write_str("IdempotencyIntent([redacted])")
76 }
77}
78
79impl Drop for IdempotencyIntent<'_> {
80 fn drop(&mut self) {
81 sanitize_bytes(self.bytes);
82 }
83}
84
85fn validate_source(source: &[u8]) -> Result<(), IdempotencyIntentError> {
86 if source.len() < MIN_IDEMPOTENCY_INTENT_BYTES {
87 return Err(IdempotencyIntentError::TooShort);
88 }
89 if source.len() > MAX_IDEMPOTENCY_INTENT_BYTES {
90 return Err(IdempotencyIntentError::TooLong);
91 }
92 let mut any_nonzero = Choice::from(0);
93 for byte in source {
94 any_nonzero |= !byte.ct_eq(&0);
95 }
96 if !bool::from(any_nonzero) {
97 return Err(IdempotencyIntentError::AllZero);
98 }
99 Ok(())
100}
101
102pub struct IdempotencyBinding<'a> {
108 intent: IdempotencyIntent<'a>,
109 fingerprint: FingerprintRef<'a>,
110}
111
112impl<'a> IdempotencyBinding<'a> {
113 #[must_use]
115 pub const fn bind(intent: IdempotencyIntent<'a>, fingerprint: FingerprintRef<'a>) -> Self {
116 Self {
117 intent,
118 fingerprint,
119 }
120 }
121
122 #[must_use]
124 pub const fn intent_len(&self) -> usize {
125 self.intent.len()
126 }
127
128 pub(crate) fn matches(&self, fingerprint: FingerprintRef<'_>) -> bool {
129 self.fingerprint.matches(fingerprint)
130 }
131}
132
133impl fmt::Debug for IdempotencyBinding<'_> {
134 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
135 formatter
136 .debug_struct("IdempotencyBinding")
137 .field("intent_len", &self.intent.len())
138 .field("fingerprint", &"[redacted]")
139 .finish()
140 }
141}