1use core::fmt;
4
5use cloud_sdk_sanitization::sanitize_bytes;
6use subtle::ConstantTimeEq;
7
8use super::{
9 AttemptBudget, PermitContext, PermitIdempotencyKey, PermitScope, PermitValidity, PlanChange,
10 PlanCost, PlanFingerprintScope, ReplayPolicy,
11};
12use crate::buffer::{encode_snapshot_bounded, measure_snapshot_bounded};
13use crate::operation::{CostIntent, OperationImpact, PreparedRequest};
14use crate::retry::{DigestAlgorithm, FingerprintHasher};
15use crate::transport::EndpointIdentity;
16
17mod encoding;
18mod error;
19use encoding::encode;
20pub use error::PlanFingerprintBuildError;
21use error::map_infallible;
22
23const DOMAIN: &[u8] = b"cloud-sdk/plan-confirm/v1\0";
24pub const MAX_CANONICAL_PLAN_BYTES: usize = 16_777_216;
26
27#[derive(Clone, Copy)]
29pub struct PlanConfirmation<'a, 'request> {
30 prepared: PreparedRequest<'request>,
31 endpoint: EndpointIdentity<'a>,
32 account: PlanFingerprintScope<'a>,
33 tenant: PlanFingerprintScope<'a>,
34 context: PermitContext<'a>,
35 validity: PermitValidity,
36 replay: ReplayPolicy,
37 attempts: AttemptBudget,
38 change: PlanChange,
39 cost: Option<PlanCost>,
40 idempotency: Option<PermitIdempotencyKey<'a>>,
41}
42
43impl<'a, 'request> PlanConfirmation<'a, 'request> {
44 #[allow(clippy::too_many_arguments)]
46 #[must_use]
47 pub const fn new(
48 prepared: PreparedRequest<'request>,
49 endpoint: EndpointIdentity<'a>,
50 account: PlanFingerprintScope<'a>,
51 tenant: PlanFingerprintScope<'a>,
52 context: PermitContext<'a>,
53 validity: PermitValidity,
54 replay: ReplayPolicy,
55 attempts: AttemptBudget,
56 change: PlanChange,
57 cost: Option<PlanCost>,
58 idempotency: Option<PermitIdempotencyKey<'a>>,
59 ) -> Self {
60 Self {
61 prepared,
62 endpoint,
63 account,
64 tenant,
65 context,
66 validity,
67 replay,
68 attempts,
69 change,
70 cost,
71 idempotency,
72 }
73 }
74}
75
76impl fmt::Debug for PlanConfirmation<'_, '_> {
77 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78 formatter
79 .debug_struct("PlanConfirmation")
80 .field("prepared", &self.prepared)
81 .field("endpoint", &self.endpoint)
82 .field("account", &"[redacted]")
83 .field("tenant", &"[redacted]")
84 .field("context", &"[redacted]")
85 .field("validity", &self.validity)
86 .field("replay", &self.replay)
87 .field("attempts", &self.attempts)
88 .field("change", &self.change)
89 .field("cost", &self.cost)
90 .field("idempotency", &"[redacted]")
91 .finish()
92 }
93}
94
95pub struct CanonicalPlanFingerprint<'output, 'plan, 'request> {
97 storage: &'output mut [u8],
98 len: usize,
99 plan: PlanConfirmation<'plan, 'request>,
100 scope: PermitScope,
101}
102
103impl<'output, 'plan, 'request> CanonicalPlanFingerprint<'output, 'plan, 'request> {
104 #[must_use]
106 pub fn as_ref(&self) -> PlanFingerprintRef<'_> {
107 PlanFingerprintRef(PlanFingerprintKind::Exact(self.bytes()))
108 }
109
110 #[must_use]
112 pub fn subject(&self) -> PlanSubject<'request, '_> {
113 subject(&self.plan, self.scope, self.as_ref())
114 }
115
116 #[must_use]
118 pub const fn len(&self) -> usize {
119 self.len
120 }
121
122 #[must_use]
124 pub const fn is_empty(&self) -> bool {
125 false
126 }
127
128 fn bytes(&self) -> &[u8] {
129 self.storage.get(..self.len).unwrap_or_default()
130 }
131}
132
133impl fmt::Debug for CanonicalPlanFingerprint<'_, '_, '_> {
134 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
135 formatter
136 .debug_struct("CanonicalPlanFingerprint")
137 .field("len", &self.len)
138 .field("scope", &self.scope)
139 .field("bytes", &"[redacted]")
140 .finish()
141 }
142}
143
144impl Drop for CanonicalPlanFingerprint<'_, '_, '_> {
145 fn drop(&mut self) {
146 sanitize_bytes(self.storage);
147 self.len = 0;
148 }
149}
150
151pub struct PlanFingerprintDigest<'output, 'plan, 'request> {
153 algorithm: DigestAlgorithm,
154 storage: &'output mut [u8],
155 len: usize,
156 plan: PlanConfirmation<'plan, 'request>,
157 scope: PermitScope,
158}
159
160impl PlanFingerprintDigest<'_, '_, '_> {
161 #[must_use]
163 pub const fn algorithm(&self) -> DigestAlgorithm {
164 self.algorithm
165 }
166
167 #[must_use]
169 pub fn as_ref(&self) -> PlanFingerprintRef<'_> {
170 PlanFingerprintRef(PlanFingerprintKind::Digest {
171 algorithm: self.algorithm,
172 bytes: self.storage.get(..self.len).unwrap_or_default(),
173 })
174 }
175
176 #[must_use]
178 pub fn subject(&self) -> PlanSubject<'_, '_> {
179 subject(&self.plan, self.scope, self.as_ref())
180 }
181}
182
183impl fmt::Debug for PlanFingerprintDigest<'_, '_, '_> {
184 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
185 formatter
186 .debug_struct("PlanFingerprintDigest")
187 .field("algorithm", &self.algorithm)
188 .field("scope", &self.scope)
189 .field("bytes", &"[redacted]")
190 .finish()
191 }
192}
193
194impl Drop for PlanFingerprintDigest<'_, '_, '_> {
195 fn drop(&mut self) {
196 sanitize_bytes(self.storage);
197 self.len = 0;
198 }
199}
200
201#[derive(Clone, Copy)]
203pub struct PlanFingerprintRef<'a>(PlanFingerprintKind<'a>);
204
205#[derive(Clone, Copy)]
206enum PlanFingerprintKind<'a> {
207 Exact(&'a [u8]),
208 Digest {
209 algorithm: DigestAlgorithm,
210 bytes: &'a [u8],
211 },
212}
213
214impl PlanFingerprintRef<'_> {
215 pub(crate) fn matches(self, other: Self) -> bool {
216 match (self.0, other.0) {
217 (PlanFingerprintKind::Exact(left), PlanFingerprintKind::Exact(right)) => {
218 constant_time_eq(left, right)
219 }
220 (
221 PlanFingerprintKind::Digest {
222 algorithm: left,
223 bytes: left_bytes,
224 },
225 PlanFingerprintKind::Digest {
226 algorithm: right,
227 bytes: right_bytes,
228 },
229 ) => left == right && constant_time_eq(left_bytes, right_bytes),
230 _ => false,
231 }
232 }
233}
234
235impl fmt::Debug for PlanFingerprintRef<'_> {
236 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
237 formatter.write_str("PlanFingerprintRef([redacted])")
238 }
239}
240
241#[derive(Clone, Copy)]
243pub struct PlanSubject<'request, 'fingerprint> {
244 prepared: &'fingerprint PreparedRequest<'request>,
245 fingerprint: PlanFingerprintRef<'fingerprint>,
246 endpoint: EndpointIdentity<'fingerprint>,
247 scope: PermitScope,
248 validity: PermitValidity,
249 replay: ReplayPolicy,
250 attempts: AttemptBudget,
251 idempotency: Option<PermitIdempotencyKey<'fingerprint>>,
252}
253
254impl<'request, 'fingerprint> PlanSubject<'request, 'fingerprint> {
255 #[must_use]
257 pub const fn scope(self) -> PermitScope {
258 self.scope
259 }
260
261 #[must_use]
263 pub const fn replay_policy(self) -> ReplayPolicy {
264 self.replay
265 }
266
267 #[must_use]
269 pub const fn attempt_budget(self) -> AttemptBudget {
270 self.attempts
271 }
272
273 pub(crate) const fn endpoint(self) -> EndpointIdentity<'fingerprint> {
274 self.endpoint
275 }
276
277 pub(crate) const fn prepared(self) -> PreparedRequest<'request> {
278 *self.prepared
279 }
280
281 pub(crate) const fn fingerprint(self) -> PlanFingerprintRef<'fingerprint> {
282 self.fingerprint
283 }
284
285 pub(crate) const fn validity(self) -> PermitValidity {
286 self.validity
287 }
288
289 pub(crate) const fn idempotency(self) -> Option<PermitIdempotencyKey<'fingerprint>> {
290 self.idempotency
291 }
292}
293
294impl fmt::Debug for PlanSubject<'_, '_> {
295 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
296 formatter
297 .debug_struct("PlanSubject")
298 .field("prepared", &self.prepared)
299 .field("fingerprint", &"[redacted]")
300 .field("endpoint", &self.endpoint)
301 .field("scope", &self.scope)
302 .field("validity", &self.validity)
303 .field("replay", &self.replay)
304 .field("attempts", &self.attempts)
305 .field("idempotency", &"[redacted]")
306 .finish()
307 }
308}
309
310pub fn build_canonical_plan<'output, 'plan, 'request>(
312 plan: PlanConfirmation<'plan, 'request>,
313 output: &'output mut [u8],
314) -> Result<
315 CanonicalPlanFingerprint<'output, 'plan, 'request>,
316 PlanFingerprintBuildError<core::convert::Infallible>,
317> {
318 sanitize_bytes(output);
319 let scope = validate(&plan)?;
320 let required = measure_snapshot_bounded(
321 &plan,
322 MAX_CANONICAL_PLAN_BYTES,
323 PlanFingerprintBuildError::InputTooLarge,
324 encode,
325 )?;
326 if output.len() < required {
327 return Err(PlanFingerprintBuildError::OutputTooSmall);
328 }
329 let len = encode_snapshot_bounded(
330 &plan,
331 output,
332 MAX_CANONICAL_PLAN_BYTES,
333 PlanFingerprintBuildError::InputTooLarge,
334 encode,
335 )?;
336 Ok(CanonicalPlanFingerprint {
337 storage: output,
338 len,
339 plan,
340 scope,
341 })
342}
343
344pub fn build_plan_digest<'output, 'plan, 'request, H: FingerprintHasher>(
346 plan: PlanConfirmation<'plan, 'request>,
347 scratch: &mut [u8],
348 output: &'output mut [u8],
349 hasher: &H,
350) -> Result<PlanFingerprintDigest<'output, 'plan, 'request>, PlanFingerprintBuildError<H::Error>> {
351 sanitize_bytes(output);
352 let exact = build_canonical_plan(plan, scratch).map_err(map_infallible)?;
353 let algorithm = hasher.algorithm();
354 let expected = algorithm.output_len();
355 if output.len() < expected {
356 return Err(PlanFingerprintBuildError::OutputTooSmall);
357 }
358 let mut rollback = DigestRollback::new(output);
359 let len = hasher
360 .digest(exact.bytes(), rollback.target(expected))
361 .map_err(PlanFingerprintBuildError::Hasher)?;
362 if len != expected {
363 return Err(PlanFingerprintBuildError::InvalidDigestLength);
364 }
365 let output = rollback.disarm();
366 Ok(PlanFingerprintDigest {
367 algorithm,
368 storage: output,
369 len,
370 plan,
371 scope: exact.scope,
372 })
373}
374
375struct DigestRollback<'a> {
376 output: &'a mut [u8],
377 armed: bool,
378}
379
380impl<'a> DigestRollback<'a> {
381 fn new(output: &'a mut [u8]) -> Self {
382 Self {
383 output,
384 armed: true,
385 }
386 }
387
388 fn target(&mut self, len: usize) -> &mut [u8] {
389 self.output.get_mut(..len).unwrap_or_default()
390 }
391
392 fn disarm(mut self) -> &'a mut [u8] {
393 self.armed = false;
394 core::mem::take(&mut self.output)
395 }
396}
397
398impl Drop for DigestRollback<'_> {
399 fn drop(&mut self) {
400 if self.armed {
401 sanitize_bytes(self.output);
402 }
403 }
404}
405
406pub(super) fn validate<E>(
407 plan: &PlanConfirmation<'_, '_>,
408) -> Result<PermitScope, PlanFingerprintBuildError<E>> {
409 if plan.prepared.operation_id().is_none() {
410 return Err(PlanFingerprintBuildError::MissingOperationId);
411 }
412 if !plan
413 .prepared
414 .service()
415 .endpoint_policy()
416 .admits(plan.endpoint)
417 {
418 return Err(PlanFingerprintBuildError::EndpointNotAdmitted);
419 }
420 if plan.change == PlanChange::NoOp {
421 return Err(PlanFingerprintBuildError::NoOp);
422 }
423 plan.account
424 .bytes()
425 .map_err(PlanFingerprintBuildError::Context)?;
426 plan.tenant
427 .bytes()
428 .map_err(PlanFingerprintBuildError::Context)?;
429 let metadata = plan.prepared.metadata();
430 let scope = match (metadata.cost_intent(), metadata.impact()) {
431 (CostIntent::MayIncurCost, _) => PermitScope::Cost,
432 (_, OperationImpact::Destructive) => PermitScope::Destructive,
433 (_, OperationImpact::Mutation) => PermitScope::Mutation,
434 (_, OperationImpact::ReadOnly) => return Err(PlanFingerprintBuildError::ReadOnlyOperation),
435 };
436 match (scope, plan.cost) {
437 (PermitScope::Cost, None) => return Err(PlanFingerprintBuildError::MissingCost),
438 (PermitScope::Mutation | PermitScope::Destructive, Some(_)) => {
439 return Err(PlanFingerprintBuildError::UnexpectedCost);
440 }
441 _ => {}
442 }
443 match (plan.replay, plan.attempts.get(), plan.idempotency) {
444 (ReplayPolicy::SingleAttempt, 1, None)
445 | (ReplayPolicy::RecoverNotSent, _, None)
446 | (ReplayPolicy::ReconcileThenRetry, _, Some(_)) => {}
447 (ReplayPolicy::SingleAttempt, _, _) => {
448 return Err(PlanFingerprintBuildError::InvalidSingleAttemptBudget);
449 }
450 (ReplayPolicy::ReconcileThenRetry, _, None) => {
451 return Err(PlanFingerprintBuildError::MissingIdempotency);
452 }
453 (_, _, Some(_)) => return Err(PlanFingerprintBuildError::UnexpectedIdempotency),
454 }
455 Ok(scope)
456}
457
458fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
459 left.len() == right.len() && bool::from(left.ct_eq(right))
460}
461
462fn subject<'request, 'plan: 'fingerprint, 'fingerprint>(
463 plan: &'fingerprint PlanConfirmation<'plan, 'request>,
464 scope: PermitScope,
465 fingerprint: PlanFingerprintRef<'fingerprint>,
466) -> PlanSubject<'request, 'fingerprint> {
467 PlanSubject {
468 prepared: &plan.prepared,
469 fingerprint,
470 endpoint: plan.endpoint,
471 scope,
472 validity: plan.validity,
473 replay: plan.replay,
474 attempts: plan.attempts,
475 idempotency: plan.idempotency,
476 }
477}