1use alloc::vec::Vec;
2
3use super::{
4 ClientBindingState, ClientParticipantAggregate, DetachReplayStatus, DetachReplayTerminal,
5 ExpectedOperationState, LostAuthorityKind, LostAuthorityTestimony, ReconnectAggregate,
6 RestoredExpectedOperationAbandonment, RestoredExpectedOperationAbandonmentReason,
7 SdkDetachReplayAggregate, reconnect::ReconnectMachineState, replay::DetachReplayState,
8};
9use super::{resume_decode::decode_facts, resume_encode::encode_aggregate};
10use crate::wire::{ClientRequest, CodecError};
11
12pub(super) const MAGIC: [u8; 4] = *b"LPCR";
13pub(super) const VERSION: u16 = 1;
14pub(super) const HEADER_LEN: usize = 14;
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum ClientResumeRecordSection {
19 Binding,
21 ExpectedOperation,
23 DetachReplay,
25 Reconnect,
27 Abandonment,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum ClientResumeRecordEncodeError {
34 NestedCodec {
36 section: ClientResumeRecordSection,
38 source: CodecError,
40 },
41 LengthOverflow,
43 DecoupledDetachReplay,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum ClientResumeRecordDecodeError {
53 Truncated {
55 needed: usize,
57 remaining: usize,
59 },
60 InvalidMagic {
62 presented: [u8; 4],
64 },
65 UnsupportedVersion {
67 presented: u16,
69 },
70 LengthMismatch {
72 declared: u64,
74 actual: usize,
76 },
77 InvalidTag {
79 section: ClientResumeRecordSection,
81 tag: u8,
83 },
84 NestedCodec {
86 section: ClientResumeRecordSection,
88 source: Option<CodecError>,
90 },
91 InvalidAbandonmentRequest {
94 request: crate::wire::ClientDiscriminant,
96 },
97 TrailingBytes {
99 remaining: usize,
101 },
102}
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub enum ClientResumeRestoreError {
107 BindingGenerationMismatch,
109 ContinuousAckOutstanding,
111 ReplayTerminalMismatch,
113 InvalidOperationAuthorization,
115 ExpectedBindingMismatch,
117 ActiveReplayExpectedDetachMismatch,
119 ExpectedDetachActiveReplayMismatch,
121 InvalidReconnectAuthorization,
123 LostAuthorityTestimonyMismatch,
126 PendingAbandonmentConflict,
130 CorruptRecord(ClientResumeRecordDecodeError),
132}
133
134#[derive(Debug, PartialEq, Eq)]
148pub struct ClientResumeRecord {
149 canonical: Vec<u8>,
150}
151
152impl ClientResumeRecord {
153 #[must_use]
155 pub fn encode_canonical(&self) -> Vec<u8> {
156 self.canonical.clone()
157 }
158
159 pub fn decode_canonical(input: &[u8]) -> Result<Self, ClientResumeRecordDecodeError> {
166 let _ = decode_facts(input)?;
167 Ok(Self {
168 canonical: input.to_vec(),
169 })
170 }
171
172 pub fn restore(self) -> Result<ClientParticipantAggregate, ClientResumeRestoreError> {
179 let facts =
180 decode_facts(&self.canonical).map_err(ClientResumeRestoreError::CorruptRecord)?;
181 validate_facts(&facts)?;
182 let mut expected = facts.expected;
183 let tokenless = expected
184 .as_ref()
185 .is_some_and(|expected| matches!(expected.request, ClientRequest::ObserverRecovery(_)));
186 let restored_abandonment = if tokenless {
187 expected
188 .take()
189 .map(|expected| RestoredExpectedOperationAbandonment {
190 request: expected.request,
191 reason: RestoredExpectedOperationAbandonmentReason::TokenlessAfterCrash,
192 was_issued: expected.issued,
193 })
194 } else {
195 facts.abandonment
196 };
197 if let Some(expected) = expected.as_mut()
198 && expected.issued
199 && expected.lost.is_none()
200 {
201 let kind = if matches!(expected.request, ClientRequest::Detach(_)) {
202 LostAuthorityKind::DetachTransportAttempt
203 } else {
204 LostAuthorityKind::IssuedOperationCorrelation
205 };
206 expected.lost = Some(LostAuthorityTestimony::mint(kind));
207 }
208 let mut reconnect_lost = facts.reconnect_lost;
209 if reconnect_lost.is_none() {
210 reconnect_lost = match facts.reconnect_state {
211 ReconnectMachineState::Permit { issued: true, .. } => Some(
212 LostAuthorityTestimony::mint(LostAuthorityKind::ReconnectPermit),
213 ),
214 ReconnectMachineState::Attempt { .. } => Some(LostAuthorityTestimony::mint(
215 LostAuthorityKind::ReconnectAttempt,
216 )),
217 ReconnectMachineState::Parked
218 | ReconnectMachineState::Permit { issued: false, .. }
219 | ReconnectMachineState::Online => None,
220 };
221 }
222 Ok(ClientParticipantAggregate {
223 binding: facts.binding,
224 expected,
225 next_operation_authorization: facts.next_operation_authorization,
226 detach_replay: SdkDetachReplayAggregate {
227 state: facts.replay,
228 },
229 reconnect: ReconnectAggregate {
230 state: facts.reconnect_state,
231 next_authorization: facts.next_authorization,
232 lost: reconnect_lost,
233 },
234 restored_abandonment,
235 })
236 }
237}
238
239impl ClientParticipantAggregate {
240 pub fn resume_record(&self) -> Result<ClientResumeRecord, ClientResumeRecordEncodeError> {
247 validate_live_replay_coupling(self.expected.as_ref(), &self.detach_replay.state)?;
248 Ok(ClientResumeRecord {
249 canonical: encode_aggregate(self)?,
250 })
251 }
252}
253
254fn validate_live_replay_coupling(
261 expected: Option<&ExpectedOperationState>,
262 replay: &DetachReplayState,
263) -> Result<(), ClientResumeRecordEncodeError> {
264 let active_replay = match replay {
265 DetachReplayState::Recorded {
266 request,
267 status: DetachReplayStatus::Parked | DetachReplayStatus::InFlight,
268 } => Some(request),
269 DetachReplayState::Empty | DetachReplayState::Recorded { .. } => None,
270 };
271 let expected_detach = expected.and_then(|expected| {
272 let ClientRequest::Detach(value) = &expected.request else {
273 return None;
274 };
275 Some(value)
276 });
277 match (active_replay, expected_detach) {
278 (Some(request), Some(value))
279 if value.conversation_id == request.conversation_id
280 && value.participant_id == request.participant_id
281 && value.capability_generation == request.capability_generation
282 && value.detach_attempt_token == request.detach_attempt_token =>
283 {
284 Ok(())
285 }
286 (None, None) => Ok(()),
287 (Some(_), _) | (None, Some(_)) => Err(ClientResumeRecordEncodeError::DecoupledDetachReplay),
288 }
289}
290
291impl super::ClientOperationCommit {
292 pub fn resume_record(&self) -> Result<ClientResumeRecord, ClientResumeRecordEncodeError> {
302 validate_live_replay_coupling(
303 self.aggregate.expected.as_ref(),
304 &self.aggregate.detach_replay.state,
305 )?;
306 Ok(ClientResumeRecord {
307 canonical: encode_aggregate(&self.aggregate)?,
308 })
309 }
310}
311
312pub(super) struct DecodedFacts {
313 pub(super) binding: ClientBindingState,
314 pub(super) next_operation_authorization: u64,
315 pub(super) expected: Option<ExpectedOperationState>,
316 pub(super) replay: DetachReplayState,
317 pub(super) reconnect_state: ReconnectMachineState,
318 pub(super) next_authorization: u64,
319 pub(super) reconnect_lost: Option<LostAuthorityTestimony>,
320 pub(super) abandonment: Option<RestoredExpectedOperationAbandonment>,
321}
322
323fn validate_facts(facts: &DecodedFacts) -> Result<(), ClientResumeRestoreError> {
324 if let ClientBindingState::Bound {
325 generation,
326 binding_epoch,
327 ..
328 } = facts.binding
329 && generation != binding_epoch.capability_generation
330 {
331 return Err(ClientResumeRestoreError::BindingGenerationMismatch);
332 }
333 if matches!(
334 facts.expected,
335 Some(ExpectedOperationState {
336 request: ClientRequest::ParticipantAck(_),
337 ..
338 })
339 ) {
340 return Err(ClientResumeRestoreError::ContinuousAckOutstanding);
341 }
342 if facts.expected.as_ref().is_some_and(|expected| {
343 expected.authorization == 0 || expected.authorization > facts.next_operation_authorization
344 }) {
345 return Err(ClientResumeRestoreError::InvalidOperationAuthorization);
346 }
347 if facts
348 .expected
349 .as_ref()
350 .is_some_and(|expected| !facts.binding.accepts_request(&expected.request))
351 {
352 return Err(ClientResumeRestoreError::ExpectedBindingMismatch);
353 }
354 let active_replay = match &facts.replay {
355 DetachReplayState::Recorded { request, status }
356 if matches!(
357 status,
358 DetachReplayStatus::Parked | DetachReplayStatus::InFlight
359 ) =>
360 {
361 Some((request, status))
362 }
363 DetachReplayState::Empty | DetachReplayState::Recorded { .. } => None,
364 };
365 let expected_detach = facts.expected.as_ref().and_then(|expected| {
366 let ClientRequest::Detach(value) = &expected.request else {
367 return None;
368 };
369 Some((value, expected.issued))
370 });
371 match (active_replay, expected_detach) {
372 (Some((request, status)), Some((value, issued)))
373 if value.conversation_id == request.conversation_id
374 && value.participant_id == request.participant_id
375 && value.capability_generation == request.capability_generation
376 && value.detach_attempt_token == request.detach_attempt_token
377 && ((matches!(status, DetachReplayStatus::Parked) && !issued)
378 || (matches!(status, DetachReplayStatus::InFlight) && issued)) => {}
379 (Some(_), _) => {
380 return Err(ClientResumeRestoreError::ActiveReplayExpectedDetachMismatch);
381 }
382 (None, Some(_)) => {
383 return Err(ClientResumeRestoreError::ExpectedDetachActiveReplayMismatch);
384 }
385 (None, None) => {}
386 }
387 if let DetachReplayState::Recorded {
388 request,
389 status: DetachReplayStatus::Terminal(terminal),
390 } = &facts.replay
391 && !terminal_matches(request, terminal)
392 {
393 return Err(ClientResumeRestoreError::ReplayTerminalMismatch);
394 }
395 let authorization = match facts.reconnect_state {
396 ReconnectMachineState::Permit { authorization, .. }
397 | ReconnectMachineState::Attempt { authorization, .. } => Some(authorization),
398 ReconnectMachineState::Parked | ReconnectMachineState::Online => None,
399 };
400 if authorization.is_some_and(|value| value == 0 || value > facts.next_authorization) {
401 return Err(ClientResumeRestoreError::InvalidReconnectAuthorization);
402 }
403 validate_testimony_coupling(facts)?;
404 Ok(())
405}
406
407fn validate_testimony_coupling(facts: &DecodedFacts) -> Result<(), ClientResumeRestoreError> {
412 if let Some(expected) = facts.expected.as_ref()
413 && let Some(testimony) = expected.lost.as_ref()
414 {
415 let tokenless = matches!(expected.request, ClientRequest::ObserverRecovery(_));
416 let expected_kind = if matches!(expected.request, ClientRequest::Detach(_)) {
417 LostAuthorityKind::DetachTransportAttempt
418 } else {
419 LostAuthorityKind::IssuedOperationCorrelation
420 };
421 if !expected.issued || tokenless || testimony.kind() != expected_kind {
422 return Err(ClientResumeRestoreError::LostAuthorityTestimonyMismatch);
423 }
424 }
425 if let Some(testimony) = facts.reconnect_lost.as_ref() {
426 let state_kind = match facts.reconnect_state {
427 ReconnectMachineState::Permit { issued: true, .. } => {
428 Some(LostAuthorityKind::ReconnectPermit)
429 }
430 ReconnectMachineState::Attempt { .. } => Some(LostAuthorityKind::ReconnectAttempt),
431 ReconnectMachineState::Parked
432 | ReconnectMachineState::Permit { issued: false, .. }
433 | ReconnectMachineState::Online => None,
434 };
435 if state_kind != Some(testimony.kind()) {
436 return Err(ClientResumeRestoreError::LostAuthorityTestimonyMismatch);
437 }
438 }
439 if facts.abandonment.is_some()
440 && facts
441 .expected
442 .as_ref()
443 .is_some_and(|expected| matches!(expected.request, ClientRequest::ObserverRecovery(_)))
444 {
445 return Err(ClientResumeRestoreError::PendingAbandonmentConflict);
446 }
447 Ok(())
448}
449
450fn terminal_matches(
451 request: &crate::wire::DetachEnvelope,
452 terminal: &DetachReplayTerminal,
453) -> bool {
454 match terminal {
455 DetachReplayTerminal::DetachCommitted(value) => {
456 value.conversation_id() == request.conversation_id
457 && value.participant_id() == request.participant_id
458 && value.capability_generation() == request.capability_generation
459 && value.detach_attempt_token() == request.detach_attempt_token
460 }
461 DetachReplayTerminal::DetachInProgress(value) => {
462 let expected_generation = request.capability_generation;
463 let presented_generation = value.presented_generation;
464 let expected_token = request.detach_attempt_token;
465 let presented_token = value.presented_token;
466 value.conversation_id == request.conversation_id
467 && value.participant_id == request.participant_id
468 && presented_generation == expected_generation
469 && presented_token == expected_token
470 }
471 DetachReplayTerminal::TerminalizedDetachCell(value) => {
472 value.conversation_id() == request.conversation_id
473 && value.participant_id() == request.participant_id
474 && value.capability_generation() == request.capability_generation
475 && value.detach_attempt_token() == request.detach_attempt_token
476 }
477 DetachReplayTerminal::AuthorityRefused(value) => super::correlation::matches_request(
482 value.value(),
483 &ClientRequest::Detach(crate::wire::DetachRequest {
484 conversation_id: request.conversation_id,
485 participant_id: request.participant_id,
486 capability_generation: request.capability_generation,
487 detach_attempt_token: request.detach_attempt_token,
488 }),
489 ),
490 }
491}