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}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum ClientResumeRecordDecodeError {
48 Truncated {
50 needed: usize,
52 remaining: usize,
54 },
55 InvalidMagic {
57 presented: [u8; 4],
59 },
60 UnsupportedVersion {
62 presented: u16,
64 },
65 LengthMismatch {
67 declared: u64,
69 actual: usize,
71 },
72 InvalidTag {
74 section: ClientResumeRecordSection,
76 tag: u8,
78 },
79 NestedCodec {
81 section: ClientResumeRecordSection,
83 source: Option<CodecError>,
85 },
86 InvalidAbandonmentRequest {
89 request: crate::wire::ClientDiscriminant,
91 },
92 TrailingBytes {
94 remaining: usize,
96 },
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub enum ClientResumeRestoreError {
102 BindingGenerationMismatch,
104 ContinuousAckOutstanding,
106 ReplayTerminalMismatch,
108 InvalidOperationAuthorization,
110 ExpectedBindingMismatch,
112 ActiveReplayExpectedDetachMismatch,
114 ExpectedDetachActiveReplayMismatch,
116 InvalidReconnectAuthorization,
118 LostAuthorityTestimonyMismatch,
121 PendingAbandonmentConflict,
125 CorruptRecord(ClientResumeRecordDecodeError),
127}
128
129#[derive(Debug, PartialEq, Eq)]
143pub struct ClientResumeRecord {
144 canonical: Vec<u8>,
145}
146
147impl ClientResumeRecord {
148 #[must_use]
150 pub fn encode_canonical(&self) -> Vec<u8> {
151 self.canonical.clone()
152 }
153
154 pub fn decode_canonical(input: &[u8]) -> Result<Self, ClientResumeRecordDecodeError> {
161 let _ = decode_facts(input)?;
162 Ok(Self {
163 canonical: input.to_vec(),
164 })
165 }
166
167 pub fn restore(self) -> Result<ClientParticipantAggregate, ClientResumeRestoreError> {
174 let facts =
175 decode_facts(&self.canonical).map_err(ClientResumeRestoreError::CorruptRecord)?;
176 validate_facts(&facts)?;
177 let mut expected = facts.expected;
178 let tokenless = expected
179 .as_ref()
180 .is_some_and(|expected| matches!(expected.request, ClientRequest::ObserverRecovery(_)));
181 let restored_abandonment = if tokenless {
182 expected
183 .take()
184 .map(|expected| RestoredExpectedOperationAbandonment {
185 request: expected.request,
186 reason: RestoredExpectedOperationAbandonmentReason::TokenlessAfterCrash,
187 was_issued: expected.issued,
188 })
189 } else {
190 facts.abandonment
191 };
192 if let Some(expected) = expected.as_mut()
193 && expected.issued
194 && expected.lost.is_none()
195 {
196 let kind = if matches!(expected.request, ClientRequest::Detach(_)) {
197 LostAuthorityKind::DetachTransportAttempt
198 } else {
199 LostAuthorityKind::IssuedOperationCorrelation
200 };
201 expected.lost = Some(LostAuthorityTestimony::mint(kind));
202 }
203 let mut reconnect_lost = facts.reconnect_lost;
204 if reconnect_lost.is_none() {
205 reconnect_lost = match facts.reconnect_state {
206 ReconnectMachineState::Permit { issued: true, .. } => Some(
207 LostAuthorityTestimony::mint(LostAuthorityKind::ReconnectPermit),
208 ),
209 ReconnectMachineState::Attempt { .. } => Some(LostAuthorityTestimony::mint(
210 LostAuthorityKind::ReconnectAttempt,
211 )),
212 ReconnectMachineState::Parked
213 | ReconnectMachineState::Permit { issued: false, .. }
214 | ReconnectMachineState::Online => None,
215 };
216 }
217 Ok(ClientParticipantAggregate {
218 binding: facts.binding,
219 expected,
220 next_operation_authorization: facts.next_operation_authorization,
221 detach_replay: SdkDetachReplayAggregate {
222 state: facts.replay,
223 },
224 reconnect: ReconnectAggregate {
225 state: facts.reconnect_state,
226 next_authorization: facts.next_authorization,
227 lost: reconnect_lost,
228 },
229 restored_abandonment,
230 })
231 }
232}
233
234impl ClientParticipantAggregate {
235 pub fn resume_record(&self) -> Result<ClientResumeRecord, ClientResumeRecordEncodeError> {
242 Ok(ClientResumeRecord {
243 canonical: encode_aggregate(self)?,
244 })
245 }
246}
247
248impl super::ClientOperationCommit {
249 pub fn resume_record(&self) -> Result<ClientResumeRecord, ClientResumeRecordEncodeError> {
259 Ok(ClientResumeRecord {
260 canonical: encode_aggregate(&self.aggregate)?,
261 })
262 }
263}
264
265pub(super) struct DecodedFacts {
266 pub(super) binding: ClientBindingState,
267 pub(super) next_operation_authorization: u64,
268 pub(super) expected: Option<ExpectedOperationState>,
269 pub(super) replay: DetachReplayState,
270 pub(super) reconnect_state: ReconnectMachineState,
271 pub(super) next_authorization: u64,
272 pub(super) reconnect_lost: Option<LostAuthorityTestimony>,
273 pub(super) abandonment: Option<RestoredExpectedOperationAbandonment>,
274}
275
276fn validate_facts(facts: &DecodedFacts) -> Result<(), ClientResumeRestoreError> {
277 if let ClientBindingState::Bound {
278 generation,
279 binding_epoch,
280 ..
281 } = facts.binding
282 && generation != binding_epoch.capability_generation
283 {
284 return Err(ClientResumeRestoreError::BindingGenerationMismatch);
285 }
286 if matches!(
287 facts.expected,
288 Some(ExpectedOperationState {
289 request: ClientRequest::ParticipantAck(_),
290 ..
291 })
292 ) {
293 return Err(ClientResumeRestoreError::ContinuousAckOutstanding);
294 }
295 if facts.expected.as_ref().is_some_and(|expected| {
296 expected.authorization == 0 || expected.authorization > facts.next_operation_authorization
297 }) {
298 return Err(ClientResumeRestoreError::InvalidOperationAuthorization);
299 }
300 if facts
301 .expected
302 .as_ref()
303 .is_some_and(|expected| !facts.binding.accepts_request(&expected.request))
304 {
305 return Err(ClientResumeRestoreError::ExpectedBindingMismatch);
306 }
307 let active_replay = match &facts.replay {
308 DetachReplayState::Recorded { request, status }
309 if matches!(
310 status,
311 DetachReplayStatus::Parked | DetachReplayStatus::InFlight
312 ) =>
313 {
314 Some((request, status))
315 }
316 DetachReplayState::Empty | DetachReplayState::Recorded { .. } => None,
317 };
318 let expected_detach = facts.expected.as_ref().and_then(|expected| {
319 let ClientRequest::Detach(value) = &expected.request else {
320 return None;
321 };
322 Some((value, expected.issued))
323 });
324 match (active_replay, expected_detach) {
325 (Some((request, status)), Some((value, issued)))
326 if value.conversation_id == request.conversation_id
327 && value.participant_id == request.participant_id
328 && value.capability_generation == request.capability_generation
329 && value.detach_attempt_token == request.detach_attempt_token
330 && ((matches!(status, DetachReplayStatus::Parked) && !issued)
331 || (matches!(status, DetachReplayStatus::InFlight) && issued)) => {}
332 (Some(_), _) => {
333 return Err(ClientResumeRestoreError::ActiveReplayExpectedDetachMismatch);
334 }
335 (None, Some(_)) => {
336 return Err(ClientResumeRestoreError::ExpectedDetachActiveReplayMismatch);
337 }
338 (None, None) => {}
339 }
340 if let DetachReplayState::Recorded {
341 request,
342 status: DetachReplayStatus::Terminal(terminal),
343 } = &facts.replay
344 && !terminal_matches(request, terminal)
345 {
346 return Err(ClientResumeRestoreError::ReplayTerminalMismatch);
347 }
348 let authorization = match facts.reconnect_state {
349 ReconnectMachineState::Permit { authorization, .. }
350 | ReconnectMachineState::Attempt { authorization, .. } => Some(authorization),
351 ReconnectMachineState::Parked | ReconnectMachineState::Online => None,
352 };
353 if authorization.is_some_and(|value| value == 0 || value > facts.next_authorization) {
354 return Err(ClientResumeRestoreError::InvalidReconnectAuthorization);
355 }
356 validate_testimony_coupling(facts)?;
357 Ok(())
358}
359
360fn validate_testimony_coupling(facts: &DecodedFacts) -> Result<(), ClientResumeRestoreError> {
365 if let Some(expected) = facts.expected.as_ref()
366 && let Some(testimony) = expected.lost.as_ref()
367 {
368 let tokenless = matches!(expected.request, ClientRequest::ObserverRecovery(_));
369 let expected_kind = if matches!(expected.request, ClientRequest::Detach(_)) {
370 LostAuthorityKind::DetachTransportAttempt
371 } else {
372 LostAuthorityKind::IssuedOperationCorrelation
373 };
374 if !expected.issued || tokenless || testimony.kind() != expected_kind {
375 return Err(ClientResumeRestoreError::LostAuthorityTestimonyMismatch);
376 }
377 }
378 if let Some(testimony) = facts.reconnect_lost.as_ref() {
379 let state_kind = match facts.reconnect_state {
380 ReconnectMachineState::Permit { issued: true, .. } => {
381 Some(LostAuthorityKind::ReconnectPermit)
382 }
383 ReconnectMachineState::Attempt { .. } => Some(LostAuthorityKind::ReconnectAttempt),
384 ReconnectMachineState::Parked
385 | ReconnectMachineState::Permit { issued: false, .. }
386 | ReconnectMachineState::Online => None,
387 };
388 if state_kind != Some(testimony.kind()) {
389 return Err(ClientResumeRestoreError::LostAuthorityTestimonyMismatch);
390 }
391 }
392 if facts.abandonment.is_some()
393 && facts
394 .expected
395 .as_ref()
396 .is_some_and(|expected| matches!(expected.request, ClientRequest::ObserverRecovery(_)))
397 {
398 return Err(ClientResumeRestoreError::PendingAbandonmentConflict);
399 }
400 Ok(())
401}
402
403fn terminal_matches(
404 request: &crate::wire::DetachEnvelope,
405 terminal: &DetachReplayTerminal,
406) -> bool {
407 match terminal {
408 DetachReplayTerminal::DetachCommitted(value) => {
409 value.conversation_id() == request.conversation_id
410 && value.participant_id() == request.participant_id
411 && value.capability_generation() == request.capability_generation
412 && value.detach_attempt_token() == request.detach_attempt_token
413 }
414 DetachReplayTerminal::DetachInProgress(value) => {
415 let expected_generation = request.capability_generation;
416 let presented_generation = value.presented_generation;
417 let expected_token = request.detach_attempt_token;
418 let presented_token = value.presented_token;
419 value.conversation_id == request.conversation_id
420 && value.participant_id == request.participant_id
421 && presented_generation == expected_generation
422 && presented_token == expected_token
423 }
424 DetachReplayTerminal::TerminalizedDetachCell(value) => {
425 value.conversation_id() == request.conversation_id
426 && value.participant_id() == request.participant_id
427 && value.capability_generation() == request.capability_generation
428 && value.detach_attempt_token() == request.detach_attempt_token
429 }
430 }
431}