1use std::{collections::BTreeMap, path::Path};
2
3use cookie_agent_identity::{
4 AuthFieldName, AuthMethodId, CatalogRevision, ProviderId, ProviderStateRevision,
5 ProviderStoreRevision, RuntimeRevision, SafeCode, SetupFieldId,
6};
7use jiff::Timestamp;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use sha2::{Digest as _, Sha256};
11use uuid::Uuid;
12use zeroize::{Zeroize, Zeroizing};
13
14use crate::{
15 BoundedSetupString, SafeSetupValue, Sha256Digest,
16 secure_store::{DEFAULT_LOCK_BUDGET, SecureDirectory, SecureDirectoryLock},
17};
18
19use super::{
20 PROVIDER_STORE_FILE, PROVIDER_STORE_LOCK_FILE, PROVIDER_STORE_SCHEMA_VERSION,
21 types::{
22 ClientConnectId, ClientRequestId, ConnectMutation, ConnectReceipt, DisconnectMutation,
23 DisconnectReceipt, DurableProviderReceipt, MAX_PROVIDERS, MAX_RECEIPTS, ProviderAuthValues,
24 ProviderConnectionGeneration, ProviderStoreError, ProviderStoreGeneration,
25 ProviderStoreMutation, ProviderStoreSnapshot, SecretValue, StoredManagedConnection,
26 StoredProviderPolicyProjection, validate_setup_values,
27 },
28};
29
30const MAX_STORE_BYTES: u64 = 16 * 1024 * 1024;
31const LEGACY_STORE_FILE: &str = "store-v1.json";
32const PREVIOUS_STORE_FILE: &str = "store-v2.json";
33const UNVERSIONED_STORE_FILE: &str = "store.json";
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40enum StoreFileStamp {
41 Missing,
42 Present {
43 modified: std::time::SystemTime,
44 len: u64,
45 },
46 Unknown,
47}
48
49impl StoreFileStamp {
50 fn current(path: &Path) -> Self {
51 match std::fs::metadata(path) {
52 Ok(metadata) => match metadata.modified() {
53 Ok(modified) => Self::Present {
54 modified,
55 len: metadata.len(),
56 },
57 Err(_) => Self::Unknown,
58 },
59 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Self::Missing,
60 Err(_) => Self::Unknown,
61 }
62 }
63}
64
65#[derive(Clone, Copy, Debug)]
67struct ReloadStamp {
68 file: StoreFileStamp,
69 generation: ProviderStoreGeneration,
70}
71
72#[derive(Debug)]
74pub struct ProviderStore {
75 directory: SecureDirectory,
76 reload_stamp: std::sync::Mutex<Option<ReloadStamp>>,
77 #[cfg(test)]
78 transaction_opens: std::sync::atomic::AtomicU64,
79}
80
81impl ProviderStore {
82 pub fn standard() -> Result<Self, ProviderStoreError> {
84 Self::from_directory(SecureDirectory::user_data("providers")?)
85 }
86
87 pub fn open(path: impl AsRef<Path>) -> Result<Self, ProviderStoreError> {
89 Self::from_directory(SecureDirectory::open(path)?)
90 }
91
92 pub fn open_in(
94 anchor: impl AsRef<Path>,
95 relative: impl AsRef<Path>,
96 ) -> Result<Self, ProviderStoreError> {
97 Self::from_directory(SecureDirectory::open_in(anchor, relative)?)
98 }
99
100 fn from_directory(directory: SecureDirectory) -> Result<Self, ProviderStoreError> {
101 Ok(Self {
102 directory,
103 reload_stamp: std::sync::Mutex::new(None),
104 #[cfg(test)]
105 transaction_opens: std::sync::atomic::AtomicU64::new(0),
106 })
107 }
108
109 #[must_use]
110 pub fn path(&self) -> &Path {
111 self.directory.path()
112 }
113
114 pub fn load(&self) -> Result<ProviderStoreSnapshot, ProviderStoreError> {
116 Ok(self.begin_transaction()?.state.snapshot())
117 }
118
119 pub fn reload_if_changed(
127 &self,
128 known: ProviderStoreGeneration,
129 ) -> Result<Option<ProviderStoreSnapshot>, ProviderStoreError> {
130 let file_path = self.directory.path().join(PROVIDER_STORE_FILE);
131 let current_stamp = StoreFileStamp::current(&file_path);
132 if !matches!(current_stamp, StoreFileStamp::Unknown) {
133 let stamp = self
134 .reload_stamp
135 .lock()
136 .unwrap_or_else(|poisoned| poisoned.into_inner());
137 if let Some(cached) = *stamp
138 && cached.generation == known
139 && cached.file == current_stamp
140 {
141 return Ok(None);
142 }
143 }
144 let transaction = self.begin_transaction()?;
145 let post_read_stamp = transaction.file_stamp;
148 if !matches!(post_read_stamp, StoreFileStamp::Unknown) {
149 *self
150 .reload_stamp
151 .lock()
152 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(ReloadStamp {
153 file: post_read_stamp,
154 generation: transaction.state.generation,
155 });
156 }
157 if transaction.state.generation == known {
158 Ok(None)
159 } else {
160 Ok(Some(transaction.state.snapshot()))
161 }
162 }
163
164 pub fn begin_transaction(&self) -> Result<ProviderStoreTransaction<'_>, ProviderStoreError> {
171 #[cfg(test)]
172 self.transaction_opens
173 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
174 let lock = self
175 .directory
176 .lock_within(PROVIDER_STORE_LOCK_FILE, DEFAULT_LOCK_BUDGET)?;
177 reject_obsolete_files(&lock)?;
178 let state = match lock.read(PROVIDER_STORE_FILE, MAX_STORE_BYTES)? {
179 Some(bytes) => {
180 let bytes = Zeroizing::new(bytes);
181 decode_state(bytes.as_ref())?
182 }
183 None => StoreState::fresh()?,
184 };
185 let file_stamp = StoreFileStamp::current(&self.directory.path().join(PROVIDER_STORE_FILE));
189 drop(lock);
190 Ok(ProviderStoreTransaction {
191 directory: &self.directory,
192 state,
193 file_stamp,
194 transaction_id: Uuid::now_v7(),
195 })
196 }
197}
198
199pub struct ProviderStoreTransaction<'a> {
204 directory: &'a SecureDirectory,
205 state: StoreState,
206 file_stamp: StoreFileStamp,
207 transaction_id: Uuid,
208}
209
210impl std::fmt::Debug for ProviderStoreTransaction<'_> {
211 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 formatter
213 .debug_struct("ProviderStoreTransaction")
214 .field("generation", &self.state.generation)
215 .field("store_revision", &self.state.store_revision)
216 .finish_non_exhaustive()
217 }
218}
219
220impl ProviderStoreTransaction<'_> {
221 #[must_use]
222 pub fn snapshot(&self) -> ProviderStoreSnapshot {
223 self.state.snapshot()
224 }
225
226 pub fn propose_connect(
228 &self,
229 request: &ConnectMutation,
230 current_catalog_revision: &CatalogRevision,
231 ) -> Result<ConnectProposal, ProviderStoreError> {
232 validate_setup_values(&request.setup_values)?;
233 request.policy.validate()?;
234
235 let payload_digest = connect_payload_digest(request)?;
236
237 if let Some(stored) = self.state.connect_receipts.get(&request.client_connect_id) {
238 if !digest_equal(&stored.payload_digest, &payload_digest) {
239 return Err(ProviderStoreError::IdempotencyConflict);
240 }
241 return Ok(ConnectProposal::Replay(Box::new(
242 ProviderStoreMutation::Connect {
243 durable_receipt: stored.result.durable_receipt.clone(),
244 durable_connection: stored.result.durable_connection.clone(),
245 },
246 )));
247 }
248
249 if &request.expected_catalog_revision != current_catalog_revision
250 || &request.policy.catalog_revision != current_catalog_revision
251 {
252 return Err(ProviderStoreError::CatalogRevisionConflict);
253 }
254 validate_expectation(&self.state, &request.expectation)?;
255
256 let mut candidate = self.state.clone();
257 candidate.generation = candidate.generation.checked_next()?;
258 let committed_at = Timestamp::now();
259 let connection_generation = self
260 .state
261 .providers
262 .get(&request.provider_id)
263 .map_or(Ok(ProviderConnectionGeneration::new(1)?), |connection| {
264 connection.connection_generation.checked_next()
265 })?;
266 let setup_fingerprint = setup_fingerprint(&request.setup_values)?;
267 let connection = StoredManagedConnection {
268 provider_id: request.provider_id.clone(),
269 setup_values: request.setup_values.clone(),
270 setup_fingerprint,
271 auth_method: request.auth_method.clone(),
272 auth_values: request.auth_values.clone(),
273 connection_generation,
274 policy: request.policy.clone(),
275 connected_at: committed_at,
276 };
277 let receipt = ConnectReceipt {
278 durable_receipt: DurableProviderReceipt {
279 receipt_id: Uuid::now_v7(),
280 store_revision: placeholder_revision(),
281 provider_state_revision: provider_state_revision(&placeholder_revision()),
282 committed_at,
283 },
284 durable_connection: connection.descriptor(),
285 };
286 candidate
287 .providers
288 .insert(request.provider_id.clone(), connection);
289 if candidate.providers.len() > MAX_PROVIDERS
290 || candidate.connect_receipts.len() >= MAX_RECEIPTS
291 {
292 return Err(ProviderStoreError::InvalidRequest);
293 }
294 candidate.connect_receipts.insert(
295 request.client_connect_id.clone(),
296 StoredConnectReceipt {
297 payload_digest,
298 result: receipt.clone(),
299 },
300 );
301 candidate.finalize_revision_for_connect(&request.client_connect_id)?;
302 let receipt = candidate
303 .connect_receipts
304 .get(&request.client_connect_id)
305 .expect("inserted connect receipt")
306 .result
307 .clone();
308 let proposal = ProposedProviderStore::new(
309 self.transaction_id,
310 &self.state,
311 candidate,
312 ProviderStoreMutation::Connect {
313 durable_receipt: receipt.durable_receipt,
314 durable_connection: receipt.durable_connection,
315 },
316 )?;
317 Ok(ConnectProposal::Proposed(Box::new(proposal)))
318 }
319
320 pub fn propose_disconnect(
322 &self,
323 request: &DisconnectMutation,
324 current_runtime_revision: &RuntimeRevision,
325 ) -> Result<DisconnectProposal, ProviderStoreError> {
326 let payload_digest = disconnect_payload_digest(request)?;
327 if let Some(stored) = self
328 .state
329 .disconnect_receipts
330 .get(&request.client_request_id)
331 {
332 if !digest_equal(&stored.payload_digest, &payload_digest) {
333 return Err(ProviderStoreError::IdempotencyConflict);
334 }
335 return Ok(DisconnectProposal::Replay(Box::new(
336 ProviderStoreMutation::Disconnect {
337 durable_receipt: stored.result.durable_receipt.clone(),
338 provider_id: stored.result.provider_id.clone(),
339 disconnected: stored.result.disconnected,
340 },
341 )));
342 }
343
344 if &request.expected_runtime_revision != current_runtime_revision {
345 return Err(ProviderStoreError::RuntimeRevisionConflict);
346 }
347 if request.expected_store_generation != self.state.generation {
348 return Err(ProviderStoreError::StoreGenerationConflict);
349 }
350 if request.expected_store_revision != self.state.store_revision {
351 return Err(ProviderStoreError::StoreRevisionConflict);
352 }
353 if request.expected_provider_state_revision
354 != provider_state_revision(&self.state.store_revision)
355 {
356 return Err(ProviderStoreError::ProviderStateRevisionConflict);
357 }
358 match (
359 self.state.providers.get(&request.provider_id),
360 request.expected_connection_generation,
361 ) {
362 (Some(connection), Some(expected)) if connection.connection_generation == expected => {}
363 (None, None) => {}
364 _ => return Err(ProviderStoreError::StaleConnectionGeneration),
365 }
366
367 let mut candidate = self.state.clone();
368 candidate.generation = candidate.generation.checked_next()?;
369 candidate.providers.remove(&request.provider_id);
370 if candidate.disconnect_receipts.len() >= MAX_RECEIPTS {
371 return Err(ProviderStoreError::InvalidRequest);
372 }
373 let committed_at = Timestamp::now();
374 let result = DisconnectReceipt {
375 durable_receipt: DurableProviderReceipt {
376 receipt_id: Uuid::now_v7(),
377 store_revision: placeholder_revision(),
378 provider_state_revision: provider_state_revision(&placeholder_revision()),
379 committed_at,
380 },
381 provider_id: request.provider_id.clone(),
382 disconnected: true,
383 };
384 candidate.disconnect_receipts.insert(
385 request.client_request_id.clone(),
386 StoredDisconnectReceipt {
387 payload_digest,
388 result: result.clone(),
389 },
390 );
391 candidate.finalize_revision_for_disconnect(&request.client_request_id)?;
392 let result = candidate
393 .disconnect_receipts
394 .get(&request.client_request_id)
395 .expect("inserted disconnect receipt")
396 .result
397 .clone();
398 let proposal = ProposedProviderStore::new(
399 self.transaction_id,
400 &self.state,
401 candidate,
402 ProviderStoreMutation::Disconnect {
403 durable_receipt: result.durable_receipt,
404 provider_id: result.provider_id,
405 disconnected: result.disconnected,
406 },
407 )?;
408 Ok(DisconnectProposal::Proposed(Box::new(proposal)))
409 }
410
411 pub fn commit(
418 self,
419 proposal: ProposedProviderStore,
420 ) -> Result<CommittedProviderStore, ProviderStoreError> {
421 if proposal.transaction_id != self.transaction_id
422 || proposal.base_generation != self.state.generation
423 || proposal.base_revision != self.state.store_revision
424 {
425 return Err(ProviderStoreError::ProposalMismatch);
426 }
427 let lock = self
430 .directory
431 .lock_within(PROVIDER_STORE_LOCK_FILE, DEFAULT_LOCK_BUDGET)?;
432 let live = match lock.read(PROVIDER_STORE_FILE, MAX_STORE_BYTES)? {
433 Some(bytes) => {
434 let bytes = Zeroizing::new(bytes);
435 decode_state(bytes.as_ref())?
436 }
437 None => StoreState::fresh()?,
438 };
439 if live.generation != proposal.base_generation
440 || live.store_revision != proposal.base_revision
441 {
442 drop(lock);
443 return Err(ProviderStoreError::Stale);
444 }
445 let snapshot = proposal.state.snapshot();
448 let mutation = proposal.mutation.clone();
449 self.directory
450 .atomic_replace(PROVIDER_STORE_FILE, proposal.bytes.as_ref())?;
451 drop(lock);
452 Ok(CommittedProviderStore { snapshot, mutation })
453 }
454}
455
456#[derive(Debug)]
458pub enum ConnectProposal {
459 Replay(Box<ProviderStoreMutation>),
460 Proposed(Box<ProposedProviderStore>),
461}
462
463#[derive(Debug)]
465pub enum DisconnectProposal {
466 Replay(Box<ProviderStoreMutation>),
467 Proposed(Box<ProposedProviderStore>),
468}
469
470pub struct ProposedProviderStore {
472 transaction_id: Uuid,
473 base_generation: ProviderStoreGeneration,
474 base_revision: ProviderStoreRevision,
475 state: StoreState,
476 bytes: Zeroizing<Vec<u8>>,
477 mutation: ProviderStoreMutation,
478}
479
480impl ProposedProviderStore {
481 fn new(
482 transaction_id: Uuid,
483 base: &StoreState,
484 state: StoreState,
485 mutation: ProviderStoreMutation,
486 ) -> Result<Self, ProviderStoreError> {
487 state.validate()?;
488 let bytes = Zeroizing::new(encode_state(&state)?);
489 if bytes.len() as u64 > MAX_STORE_BYTES {
490 return Err(ProviderStoreError::InvalidStore);
491 }
492 Ok(Self {
493 transaction_id,
494 base_generation: base.generation,
495 base_revision: base.store_revision.clone(),
496 state,
497 bytes,
498 mutation,
499 })
500 }
501
502 #[must_use]
503 pub fn snapshot(&self) -> ProviderStoreSnapshot {
504 self.state.snapshot()
505 }
506
507 #[must_use]
508 pub fn mutation(&self) -> &ProviderStoreMutation {
509 &self.mutation
510 }
511}
512
513impl std::fmt::Debug for ProposedProviderStore {
514 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515 formatter
516 .debug_struct("ProposedProviderStore")
517 .field("base_generation", &self.base_generation)
518 .field("base_revision", &self.base_revision)
519 .field("generation", &self.state.generation)
520 .field("store_revision", &self.state.store_revision)
521 .field("mutation", &self.mutation)
522 .finish()
523 }
524}
525
526#[derive(Debug)]
528pub struct CommittedProviderStore {
529 pub snapshot: ProviderStoreSnapshot,
530 pub mutation: ProviderStoreMutation,
531}
532
533#[derive(Clone)]
534struct StoreState {
535 generation: ProviderStoreGeneration,
536 store_revision: ProviderStoreRevision,
537 providers: BTreeMap<ProviderId, StoredManagedConnection>,
538 connect_receipts: BTreeMap<ClientConnectId, StoredConnectReceipt>,
539 disconnect_receipts: BTreeMap<ClientRequestId, StoredDisconnectReceipt>,
540}
541
542impl StoreState {
543 fn fresh() -> Result<Self, ProviderStoreError> {
544 let mut state = Self {
545 generation: ProviderStoreGeneration::new(1).expect("valid initial generation"),
546 store_revision: placeholder_revision(),
547 providers: BTreeMap::new(),
548 connect_receipts: BTreeMap::new(),
549 disconnect_receipts: BTreeMap::new(),
550 };
551 state.store_revision = state_revision(&state)?;
552 Ok(state)
553 }
554
555 fn finalize_revision_for_connect(
556 &mut self,
557 id: &ClientConnectId,
558 ) -> Result<(), ProviderStoreError> {
559 let revision = state_revision(self)?;
560 self.store_revision = revision.clone();
561 let receipt = self
562 .connect_receipts
563 .get_mut(id)
564 .ok_or(ProviderStoreError::InvalidStore)?;
565 receipt.result.durable_receipt.store_revision = revision.clone();
566 receipt.result.durable_receipt.provider_state_revision = provider_state_revision(&revision);
567 Ok(())
568 }
569
570 fn finalize_revision_for_disconnect(
571 &mut self,
572 id: &ClientRequestId,
573 ) -> Result<(), ProviderStoreError> {
574 let revision = state_revision(self)?;
575 self.store_revision = revision.clone();
576 let receipt = self
577 .disconnect_receipts
578 .get_mut(id)
579 .ok_or(ProviderStoreError::InvalidStore)?;
580 receipt.result.durable_receipt.store_revision = revision.clone();
581 receipt.result.durable_receipt.provider_state_revision = provider_state_revision(&revision);
582 Ok(())
583 }
584
585 fn snapshot(&self) -> ProviderStoreSnapshot {
586 ProviderStoreSnapshot {
587 generation: self.generation,
588 store_revision: self.store_revision.clone(),
589 providers: self.providers.clone(),
590 connect_receipts: self
591 .connect_receipts
592 .iter()
593 .map(|(id, receipt)| (id.clone(), receipt.result.clone()))
594 .collect(),
595 disconnect_receipts: self
596 .disconnect_receipts
597 .iter()
598 .map(|(id, receipt)| (id.clone(), receipt.result.clone()))
599 .collect(),
600 }
601 }
602
603 fn validate(&self) -> Result<(), ProviderStoreError> {
604 if self.providers.len() > MAX_PROVIDERS
605 || self.connect_receipts.len() > MAX_RECEIPTS
606 || self.disconnect_receipts.len() > MAX_RECEIPTS
607 {
608 return Err(ProviderStoreError::InvalidStore);
609 }
610 if state_revision(self)? != self.store_revision {
611 return Err(ProviderStoreError::InvalidStore);
612 }
613 for (provider_id, connection) in &self.providers {
614 if provider_id != &connection.provider_id {
615 return Err(ProviderStoreError::InvalidStore);
616 }
617 validate_setup_values(&connection.setup_values)
618 .map_err(|_| ProviderStoreError::InvalidStore)?;
619 if setup_fingerprint(&connection.setup_values)? != connection.setup_fingerprint {
620 return Err(ProviderStoreError::InvalidStore);
621 }
622 connection.policy.validate()?;
623 }
624 for receipt in self.connect_receipts.values() {
625 validate_setup_values(&receipt.result.durable_connection.setup_values)
626 .map_err(|_| ProviderStoreError::InvalidStore)?;
627 if receipt.result.durable_connection.setup_fingerprint
628 != setup_fingerprint(&receipt.result.durable_connection.setup_values)?
629 || receipt.result.durable_connection.credential_fields.len()
630 > super::types::MAX_AUTH_FIELDS
631 || receipt
632 .result
633 .durable_connection
634 .credential_fields
635 .windows(2)
636 .any(|pair| pair[0] >= pair[1])
637 || receipt.result.durable_receipt.provider_state_revision
638 != provider_state_revision(&receipt.result.durable_receipt.store_revision)
639 {
640 return Err(ProviderStoreError::InvalidStore);
641 }
642 }
643 for receipt in self.disconnect_receipts.values() {
644 if !receipt.result.disconnected
645 || receipt.result.durable_receipt.provider_state_revision
646 != provider_state_revision(&receipt.result.durable_receipt.store_revision)
647 {
648 return Err(ProviderStoreError::InvalidStore);
649 }
650 }
651 if (!self.connect_receipts.is_empty() || !self.disconnect_receipts.is_empty())
652 && !self
653 .connect_receipts
654 .values()
655 .map(|receipt| &receipt.result.durable_receipt.store_revision)
656 .chain(
657 self.disconnect_receipts
658 .values()
659 .map(|receipt| &receipt.result.durable_receipt.store_revision),
660 )
661 .any(|revision| revision == &self.store_revision)
662 {
663 return Err(ProviderStoreError::InvalidStore);
664 }
665 Ok(())
666 }
667}
668
669#[derive(Clone)]
670struct StoredConnectReceipt {
671 payload_digest: Sha256Digest,
672 result: ConnectReceipt,
673}
674
675#[derive(Clone)]
676struct StoredDisconnectReceipt {
677 payload_digest: Sha256Digest,
678 result: DisconnectReceipt,
679}
680
681#[derive(Serialize)]
682#[serde(deny_unknown_fields)]
683struct ConnectPayload<'a> {
684 client_connect_id: &'a ClientConnectId,
685 provider_id: &'a ProviderId,
686 expected_catalog_revision: &'a CatalogRevision,
687 setup_values: &'a BTreeMap<SetupFieldId, SafeSetupValue>,
688 auth_method: &'a AuthMethodId,
689 auth_values: SecretMapRef<'a>,
690 policy: &'a StoredProviderPolicyProjection,
691}
692
693#[derive(Clone, Copy)]
694struct SecretMapRef<'a>(&'a ProviderAuthValues);
695
696impl Serialize for SecretMapRef<'_> {
697 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
698 where
699 S: serde::Serializer,
700 {
701 use serde::ser::SerializeMap as _;
702
703 let mut map = serializer.serialize_map(Some(self.0.0.len()))?;
704 for (field, value) in &self.0.0 {
705 map.serialize_entry(field, value.expose())?;
706 }
707 map.end()
708 }
709}
710
711#[derive(Serialize)]
712#[serde(deny_unknown_fields)]
713struct DisconnectPayload<'a> {
714 client_request_id: &'a ClientRequestId,
715 provider_id: &'a ProviderId,
716 expected_runtime_revision: &'a RuntimeRevision,
717 expected_provider_state_revision: &'a ProviderStateRevision,
718 expected_connection_generation: Option<ProviderConnectionGeneration>,
719}
720
721fn connect_payload_digest(request: &ConnectMutation) -> Result<Sha256Digest, ProviderStoreError> {
722 sha256_jcs(&ConnectPayload {
723 client_connect_id: &request.client_connect_id,
724 provider_id: &request.provider_id,
725 expected_catalog_revision: &request.expected_catalog_revision,
726 setup_values: &request.setup_values,
727 auth_method: &request.auth_method,
728 auth_values: SecretMapRef(&request.auth_values),
729 policy: &request.policy,
730 })
731}
732
733fn disconnect_payload_digest(
734 request: &DisconnectMutation,
735) -> Result<Sha256Digest, ProviderStoreError> {
736 sha256_jcs(&DisconnectPayload {
737 client_request_id: &request.client_request_id,
738 provider_id: &request.provider_id,
739 expected_runtime_revision: &request.expected_runtime_revision,
740 expected_provider_state_revision: &request.expected_provider_state_revision,
741 expected_connection_generation: request.expected_connection_generation,
742 })
743}
744
745fn reject_obsolete_files(lock: &SecureDirectoryLock<'_>) -> Result<(), ProviderStoreError> {
746 if lock.read(LEGACY_STORE_FILE, MAX_STORE_BYTES)?.is_some()
747 || lock.read(PREVIOUS_STORE_FILE, MAX_STORE_BYTES)?.is_some()
748 {
749 return Err(ProviderStoreError::LegacyStoreVersion);
750 }
751 if lock
752 .read(UNVERSIONED_STORE_FILE, MAX_STORE_BYTES)?
753 .is_some()
754 {
755 return Err(ProviderStoreError::UnversionedStore);
756 }
757 Ok(())
758}
759
760fn validate_expectation(
761 state: &StoreState,
762 expectation: &super::types::ProviderStoreExpectation,
763) -> Result<(), ProviderStoreError> {
764 if expectation.generation != state.generation {
765 return Err(ProviderStoreError::StoreGenerationConflict);
766 }
767 if expectation.store_revision != state.store_revision {
768 return Err(ProviderStoreError::StoreRevisionConflict);
769 }
770 if expectation.provider_state_revision != provider_state_revision(&state.store_revision) {
771 return Err(ProviderStoreError::ProviderStateRevisionConflict);
772 }
773 Ok(())
774}
775
776pub(crate) fn setup_fingerprint(
777 setup: &BTreeMap<SetupFieldId, SafeSetupValue>,
778) -> Result<Sha256Digest, ProviderStoreError> {
779 sha256_jcs(&disk_setup_values(setup))
780}
781
782fn sha256_jcs(value: &impl Serialize) -> Result<Sha256Digest, ProviderStoreError> {
783 let bytes = jcs_bytes(value)?;
784 let mut hasher = Sha256::new();
785 hasher.update(&*bytes);
786 Sha256Digest::new(format!("{:x}", hasher.finalize())).map_err(|_| ProviderStoreError::Encoding)
787}
788
789fn digest_equal(left: &Sha256Digest, right: &Sha256Digest) -> bool {
790 left.as_str()
791 .bytes()
792 .zip(right.as_str().bytes())
793 .fold(0_u8, |difference, (left, right)| {
794 difference | (left ^ right)
795 })
796 == 0
797}
798
799fn placeholder_revision() -> ProviderStoreRevision {
800 let digest = Sha256::digest([]);
801 ProviderStoreRevision::new(format!("sha256:{digest:x}")).expect("SHA-256 is a valid revision")
802}
803
804fn state_revision(state: &StoreState) -> Result<ProviderStoreRevision, ProviderStoreError> {
805 let digest = sha256_jcs(&RevisionState::from(state))?;
806 ProviderStoreRevision::new(format!("sha256:{}", digest.as_str()))
807 .map_err(|_| ProviderStoreError::Encoding)
808}
809
810fn provider_state_revision(revision: &ProviderStoreRevision) -> ProviderStateRevision {
811 ProviderStateRevision::new(revision.as_str().to_owned())
812 .expect("provider-store revision is a provider-state revision")
813}
814
815#[derive(Serialize)]
818#[serde(deny_unknown_fields)]
819struct RevisionState {
820 schema_version: u32,
821 generation: ProviderStoreGeneration,
822 providers: BTreeMap<ProviderId, DiskConnection>,
823 connect_receipts: BTreeMap<ClientConnectId, RevisionConnectReceipt>,
824 disconnect_receipts: BTreeMap<ClientRequestId, RevisionDisconnectReceipt>,
825}
826
827#[derive(Serialize)]
828#[serde(deny_unknown_fields)]
829struct RevisionConnectReceipt {
830 payload_digest: Sha256Digest,
831 result: RevisionConnectResult,
832}
833
834#[derive(Serialize)]
835#[serde(deny_unknown_fields)]
836struct RevisionConnectResult {
837 durable_receipt: RevisionDurableReceipt,
838 durable_connection: DiskConnectionDescriptor,
839}
840
841#[derive(Serialize)]
842#[serde(deny_unknown_fields)]
843struct RevisionDisconnectReceipt {
844 payload_digest: Sha256Digest,
845 result: RevisionDisconnectResult,
846}
847
848#[derive(Serialize)]
849#[serde(deny_unknown_fields)]
850struct RevisionDisconnectResult {
851 durable_receipt: RevisionDurableReceipt,
852 provider_id: ProviderId,
853 disconnected: bool,
854}
855
856#[derive(Serialize)]
857#[serde(deny_unknown_fields)]
858struct RevisionDurableReceipt {
859 receipt_id: Uuid,
860 committed_at: Timestamp,
861}
862
863impl From<&StoreState> for RevisionState {
864 fn from(state: &StoreState) -> Self {
865 Self {
866 schema_version: PROVIDER_STORE_SCHEMA_VERSION,
867 generation: state.generation,
868 providers: state
869 .providers
870 .iter()
871 .map(|(id, connection)| (id.clone(), DiskConnection::from(connection)))
872 .collect(),
873 connect_receipts: state
874 .connect_receipts
875 .iter()
876 .map(|(id, receipt)| {
877 (
878 id.clone(),
879 RevisionConnectReceipt {
880 payload_digest: receipt.payload_digest.clone(),
881 result: RevisionConnectResult {
882 durable_receipt: RevisionDurableReceipt::from(
883 &receipt.result.durable_receipt,
884 ),
885 durable_connection: DiskConnectionDescriptor::from(
886 &receipt.result.durable_connection,
887 ),
888 },
889 },
890 )
891 })
892 .collect(),
893 disconnect_receipts: state
894 .disconnect_receipts
895 .iter()
896 .map(|(id, receipt)| {
897 (
898 id.clone(),
899 RevisionDisconnectReceipt {
900 payload_digest: receipt.payload_digest.clone(),
901 result: RevisionDisconnectResult {
902 durable_receipt: RevisionDurableReceipt::from(
903 &receipt.result.durable_receipt,
904 ),
905 provider_id: receipt.result.provider_id.clone(),
906 disconnected: receipt.result.disconnected,
907 },
908 },
909 )
910 })
911 .collect(),
912 }
913 }
914}
915
916impl From<&DurableProviderReceipt> for RevisionDurableReceipt {
917 fn from(receipt: &DurableProviderReceipt) -> Self {
918 Self {
919 receipt_id: receipt.receipt_id,
920 committed_at: receipt.committed_at,
921 }
922 }
923}
924
925#[derive(Serialize, Deserialize)]
926#[serde(deny_unknown_fields)]
927struct DiskState {
928 schema_version: u32,
929 generation: ProviderStoreGeneration,
930 store_revision: ProviderStoreRevision,
931 providers: BTreeMap<ProviderId, DiskConnection>,
932 connect_receipts: BTreeMap<ClientConnectId, DiskConnectReceipt>,
933 disconnect_receipts: BTreeMap<ClientRequestId, DiskDisconnectReceipt>,
934}
935
936#[derive(Serialize, Deserialize)]
937#[serde(deny_unknown_fields)]
938struct DiskConnection {
939 provider_id: ProviderId,
940 setup_values: BTreeMap<SetupFieldId, DiskSetupValue>,
941 setup_fingerprint: Sha256Digest,
942 auth_method: AuthMethodId,
943 auth_values: BTreeMap<AuthFieldName, DiskSecret>,
944 connection_generation: ProviderConnectionGeneration,
945 policy: StoredProviderPolicyProjection,
946 connected_at: Timestamp,
947}
948
949#[derive(Serialize, Deserialize)]
950#[serde(deny_unknown_fields)]
951struct DiskConnectReceipt {
952 payload_digest: Sha256Digest,
953 result: DiskConnectResult,
954}
955
956#[derive(Clone, Serialize, Deserialize)]
957#[serde(deny_unknown_fields)]
958struct DiskConnectResult {
959 durable_receipt: DiskDurableReceipt,
960 durable_connection: DiskConnectionDescriptor,
961}
962
963#[derive(Clone, Serialize, Deserialize)]
964#[serde(deny_unknown_fields)]
965struct DiskDisconnectReceipt {
966 payload_digest: Sha256Digest,
967 result: DiskDisconnectResult,
968}
969
970#[derive(Clone, Serialize, Deserialize)]
971#[serde(deny_unknown_fields)]
972struct DiskDisconnectResult {
973 durable_receipt: DiskDurableReceipt,
974 provider_id: ProviderId,
975 disconnected: bool,
976}
977
978#[derive(Clone, Serialize, Deserialize)]
979#[serde(deny_unknown_fields)]
980struct DiskDurableReceipt {
981 receipt_id: Uuid,
982 store_revision: ProviderStoreRevision,
983 provider_state_revision: ProviderStateRevision,
984 committed_at: Timestamp,
985}
986
987#[derive(Clone, Serialize, Deserialize)]
988#[serde(deny_unknown_fields)]
989struct DiskConnectionDescriptor {
990 provider_id: ProviderId,
991 setup_values: BTreeMap<SetupFieldId, DiskSetupValue>,
992 setup_fingerprint: Sha256Digest,
993 recipe_fingerprint: Sha256Digest,
994 auth_method: AuthMethodId,
995 credential_fields: Vec<AuthFieldName>,
996 connection_generation: ProviderConnectionGeneration,
997 connected_at: Timestamp,
998}
999
1000#[derive(Serialize, Deserialize)]
1001#[serde(transparent)]
1002struct DiskSecret(String);
1003
1004#[derive(Clone, Serialize, Deserialize)]
1005#[serde(
1006 tag = "type",
1007 content = "value",
1008 rename_all = "snake_case",
1009 deny_unknown_fields
1010)]
1011enum DiskSetupValue {
1012 String(BoundedSetupString),
1013 Code(SafeCode),
1014 Integer(i64),
1015 Bool(bool),
1016}
1017
1018impl From<&SafeSetupValue> for DiskSetupValue {
1019 fn from(value: &SafeSetupValue) -> Self {
1020 match value {
1021 SafeSetupValue::String(value) => Self::String(value.clone()),
1022 SafeSetupValue::Code(value) => Self::Code(value.clone()),
1023 SafeSetupValue::Integer(value) => Self::Integer(*value),
1024 SafeSetupValue::Bool(value) => Self::Bool(*value),
1025 }
1026 }
1027}
1028
1029impl From<DiskSetupValue> for SafeSetupValue {
1030 fn from(value: DiskSetupValue) -> Self {
1031 match value {
1032 DiskSetupValue::String(value) => Self::String(value),
1033 DiskSetupValue::Code(value) => Self::Code(value),
1034 DiskSetupValue::Integer(value) => Self::Integer(value),
1035 DiskSetupValue::Bool(value) => Self::Bool(value),
1036 }
1037 }
1038}
1039
1040fn disk_setup_values(
1041 values: &BTreeMap<SetupFieldId, SafeSetupValue>,
1042) -> BTreeMap<SetupFieldId, DiskSetupValue> {
1043 values
1044 .iter()
1045 .map(|(field, value)| (field.clone(), DiskSetupValue::from(value)))
1046 .collect()
1047}
1048
1049fn live_setup_values(
1050 values: BTreeMap<SetupFieldId, DiskSetupValue>,
1051) -> BTreeMap<SetupFieldId, SafeSetupValue> {
1052 values
1053 .into_iter()
1054 .map(|(field, value)| (field, SafeSetupValue::from(value)))
1055 .collect()
1056}
1057
1058impl DiskSecret {
1059 fn into_secret(mut self) -> Result<SecretValue, ProviderStoreError> {
1060 SecretValue::new(std::mem::take(&mut self.0)).map_err(|_| ProviderStoreError::InvalidStore)
1061 }
1062}
1063
1064impl Drop for DiskSecret {
1065 fn drop(&mut self) {
1066 self.0.zeroize();
1067 }
1068}
1069
1070impl From<&SecretValue> for DiskSecret {
1071 fn from(value: &SecretValue) -> Self {
1072 Self(value.expose().to_owned())
1073 }
1074}
1075
1076fn encode_state(state: &StoreState) -> Result<Vec<u8>, ProviderStoreError> {
1077 let disk = DiskState::from(state);
1078 let mut bytes = jcs_bytes(&disk)?;
1079 Ok(std::mem::take(&mut *bytes))
1080}
1081
1082fn jcs_bytes(value: &impl Serialize) -> Result<Zeroizing<Vec<u8>>, ProviderStoreError> {
1083 let value = StrictValue(serde_json::to_value(value).map_err(|_| ProviderStoreError::Encoding)?);
1084 let mut bytes = Zeroizing::new(Vec::new());
1085 write_jcs(&value.0, &mut bytes)?;
1086 Ok(bytes)
1087}
1088
1089fn write_jcs(value: &Value, output: &mut Vec<u8>) -> Result<(), ProviderStoreError> {
1090 match value {
1091 Value::Null => output.extend_from_slice(b"null"),
1092 Value::Bool(true) => output.extend_from_slice(b"true"),
1093 Value::Bool(false) => output.extend_from_slice(b"false"),
1094 Value::Number(number) => {
1095 if number.as_i64().is_none() && number.as_u64().is_none() {
1096 return Err(ProviderStoreError::Encoding);
1097 }
1098 output.extend_from_slice(number.to_string().as_bytes());
1099 }
1100 Value::String(value) => write_jcs_string(value, output)?,
1101 Value::Array(values) => {
1102 output.push(b'[');
1103 for (index, value) in values.iter().enumerate() {
1104 if index > 0 {
1105 output.push(b',');
1106 }
1107 write_jcs(value, output)?;
1108 }
1109 output.push(b']');
1110 }
1111 Value::Object(values) => {
1112 let mut entries = values.iter().collect::<Vec<_>>();
1113 entries.sort_by(|(left, _), (right, _)| left.encode_utf16().cmp(right.encode_utf16()));
1114 output.push(b'{');
1115 for (index, (key, value)) in entries.into_iter().enumerate() {
1116 if index > 0 {
1117 output.push(b',');
1118 }
1119 write_jcs_string(key, output)?;
1120 output.push(b':');
1121 write_jcs(value, output)?;
1122 }
1123 output.push(b'}');
1124 }
1125 }
1126 Ok(())
1127}
1128
1129fn write_jcs_string(value: &str, output: &mut Vec<u8>) -> Result<(), ProviderStoreError> {
1130 let encoded =
1131 Zeroizing::new(serde_json::to_string(value).map_err(|_| ProviderStoreError::Encoding)?);
1132 output.extend_from_slice(encoded.as_bytes());
1133 Ok(())
1134}
1135
1136fn decode_state(bytes: &[u8]) -> Result<StoreState, ProviderStoreError> {
1137 let strict: StrictValue =
1138 serde_json::from_slice(bytes).map_err(|_| ProviderStoreError::InvalidStore)?;
1139 let object = strict
1140 .0
1141 .as_object()
1142 .ok_or(ProviderStoreError::InvalidStore)?;
1143 match object.get("schema_version") {
1144 None => return Err(ProviderStoreError::UnversionedStore),
1145 Some(Value::Number(number))
1146 if number.as_u64().is_some_and(|version| {
1147 version > 0 && version < u64::from(PROVIDER_STORE_SCHEMA_VERSION)
1148 }) =>
1149 {
1150 return Err(ProviderStoreError::LegacyStoreVersion);
1151 }
1152 Some(Value::Number(number))
1153 if number.as_u64() == Some(u64::from(PROVIDER_STORE_SCHEMA_VERSION)) => {}
1154 Some(_) => return Err(ProviderStoreError::UnsupportedStoreVersion),
1155 }
1156 let disk: DiskState = serde_json::from_value(strict.into_value())
1157 .map_err(|_| ProviderStoreError::InvalidStore)?;
1158 let state = StoreState::try_from(disk)?;
1159 state.validate()?;
1160 Ok(state)
1161}
1162
1163struct StrictValue(Value);
1164
1165impl StrictValue {
1166 fn into_value(mut self) -> Value {
1167 std::mem::take(&mut self.0)
1168 }
1169}
1170
1171impl Drop for StrictValue {
1172 fn drop(&mut self) {
1173 zeroize_json_value(&mut self.0);
1174 }
1175}
1176
1177fn zeroize_json_value(value: &mut Value) {
1178 match value {
1179 Value::String(value) => value.zeroize(),
1180 Value::Array(values) => {
1181 for value in values {
1182 zeroize_json_value(value);
1183 }
1184 }
1185 Value::Object(values) => {
1186 for (mut key, mut value) in std::mem::take(values) {
1187 key.zeroize();
1188 zeroize_json_value(&mut value);
1189 }
1190 }
1191 Value::Null | Value::Bool(_) | Value::Number(_) => {}
1192 }
1193}
1194
1195impl<'de> Deserialize<'de> for StrictValue {
1196 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1197 where
1198 D: serde::Deserializer<'de>,
1199 {
1200 struct Visitor;
1201 impl<'de> serde::de::Visitor<'de> for Visitor {
1202 type Value = Value;
1203
1204 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1205 formatter.write_str("strict JSON")
1206 }
1207
1208 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
1209 Ok(Value::Bool(value))
1210 }
1211
1212 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
1213 Ok(Value::Number(value.into()))
1214 }
1215
1216 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
1217 Ok(Value::Number(value.into()))
1218 }
1219
1220 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
1221 where
1222 E: serde::de::Error,
1223 {
1224 serde_json::Number::from_f64(value)
1225 .map(Value::Number)
1226 .ok_or_else(|| E::custom("invalid JSON number"))
1227 }
1228
1229 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
1230 Ok(Value::String(value.to_owned()))
1231 }
1232
1233 fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
1234 Ok(Value::String(value))
1235 }
1236
1237 fn visit_none<E>(self) -> Result<Self::Value, E> {
1238 Ok(Value::Null)
1239 }
1240
1241 fn visit_unit<E>(self) -> Result<Self::Value, E> {
1242 Ok(Value::Null)
1243 }
1244
1245 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1246 where
1247 A: serde::de::SeqAccess<'de>,
1248 {
1249 let mut values = Vec::new();
1250 while let Some(value) = sequence.next_element::<StrictValue>()? {
1251 values.push(value.into_value());
1252 }
1253 Ok(Value::Array(values))
1254 }
1255
1256 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1257 where
1258 A: serde::de::MapAccess<'de>,
1259 {
1260 let mut values = serde_json::Map::new();
1261 while let Some((key, value)) = map.next_entry::<String, StrictValue>()? {
1262 if values.insert(key, value.into_value()).is_some() {
1263 return Err(serde::de::Error::custom("duplicate JSON key"));
1264 }
1265 }
1266 Ok(Value::Object(values))
1267 }
1268 }
1269 deserializer.deserialize_any(Visitor).map(Self)
1270 }
1271}
1272
1273impl From<&StoreState> for DiskState {
1274 fn from(state: &StoreState) -> Self {
1275 Self {
1276 schema_version: PROVIDER_STORE_SCHEMA_VERSION,
1277 generation: state.generation,
1278 store_revision: state.store_revision.clone(),
1279 providers: state
1280 .providers
1281 .iter()
1282 .map(|(id, connection)| (id.clone(), DiskConnection::from(connection)))
1283 .collect(),
1284 connect_receipts: state
1285 .connect_receipts
1286 .iter()
1287 .map(|(id, receipt)| (id.clone(), DiskConnectReceipt::from(receipt)))
1288 .collect(),
1289 disconnect_receipts: state
1290 .disconnect_receipts
1291 .iter()
1292 .map(|(id, receipt)| (id.clone(), DiskDisconnectReceipt::from(receipt)))
1293 .collect(),
1294 }
1295 }
1296}
1297
1298impl TryFrom<DiskState> for StoreState {
1299 type Error = ProviderStoreError;
1300
1301 fn try_from(state: DiskState) -> Result<Self, Self::Error> {
1302 if state.schema_version != PROVIDER_STORE_SCHEMA_VERSION {
1303 return Err(ProviderStoreError::UnsupportedStoreVersion);
1304 }
1305 Ok(Self {
1306 generation: state.generation,
1307 store_revision: state.store_revision,
1308 providers: state
1309 .providers
1310 .into_iter()
1311 .map(|(id, connection)| Ok((id, StoredManagedConnection::try_from(connection)?)))
1312 .collect::<Result<_, ProviderStoreError>>()?,
1313 connect_receipts: state
1314 .connect_receipts
1315 .into_iter()
1316 .map(|(id, receipt)| Ok((id, StoredConnectReceipt::try_from(receipt)?)))
1317 .collect::<Result<_, ProviderStoreError>>()?,
1318 disconnect_receipts: state
1319 .disconnect_receipts
1320 .into_iter()
1321 .map(|(id, receipt)| Ok((id, StoredDisconnectReceipt::from(receipt))))
1322 .collect::<Result<_, ProviderStoreError>>()?,
1323 })
1324 }
1325}
1326
1327impl From<&StoredManagedConnection> for DiskConnection {
1328 fn from(connection: &StoredManagedConnection) -> Self {
1329 Self {
1330 provider_id: connection.provider_id.clone(),
1331 setup_values: disk_setup_values(&connection.setup_values),
1332 setup_fingerprint: connection.setup_fingerprint.clone(),
1333 auth_method: connection.auth_method.clone(),
1334 auth_values: connection
1335 .auth_values
1336 .0
1337 .iter()
1338 .map(|(field, value)| (field.clone(), DiskSecret::from(value)))
1339 .collect(),
1340 connection_generation: connection.connection_generation,
1341 policy: connection.policy.clone(),
1342 connected_at: connection.connected_at,
1343 }
1344 }
1345}
1346
1347impl TryFrom<DiskConnection> for StoredManagedConnection {
1348 type Error = ProviderStoreError;
1349
1350 fn try_from(connection: DiskConnection) -> Result<Self, Self::Error> {
1351 if connection.auth_values.len() > super::types::MAX_AUTH_FIELDS {
1352 return Err(ProviderStoreError::InvalidStore);
1353 }
1354 Ok(Self {
1355 provider_id: connection.provider_id,
1356 setup_values: live_setup_values(connection.setup_values),
1357 setup_fingerprint: connection.setup_fingerprint,
1358 auth_method: connection.auth_method,
1359 auth_values: ProviderAuthValues(
1360 connection
1361 .auth_values
1362 .into_iter()
1363 .map(|(field, value)| Ok((field, value.into_secret()?)))
1364 .collect::<Result<_, ProviderStoreError>>()?,
1365 ),
1366 connection_generation: connection.connection_generation,
1367 policy: connection.policy,
1368 connected_at: connection.connected_at,
1369 })
1370 }
1371}
1372
1373impl From<&StoredConnectReceipt> for DiskConnectReceipt {
1374 fn from(receipt: &StoredConnectReceipt) -> Self {
1375 Self {
1376 payload_digest: receipt.payload_digest.clone(),
1377 result: DiskConnectResult {
1378 durable_receipt: DiskDurableReceipt::from(&receipt.result.durable_receipt),
1379 durable_connection: DiskConnectionDescriptor::from(
1380 &receipt.result.durable_connection,
1381 ),
1382 },
1383 }
1384 }
1385}
1386
1387impl TryFrom<DiskConnectReceipt> for StoredConnectReceipt {
1388 type Error = ProviderStoreError;
1389
1390 fn try_from(receipt: DiskConnectReceipt) -> Result<Self, Self::Error> {
1391 Ok(Self {
1392 payload_digest: receipt.payload_digest,
1393 result: ConnectReceipt {
1394 durable_receipt: DurableProviderReceipt::from(receipt.result.durable_receipt),
1395 durable_connection: super::types::DurableConnectionDescriptor::from(
1396 receipt.result.durable_connection,
1397 ),
1398 },
1399 })
1400 }
1401}
1402
1403impl From<&StoredDisconnectReceipt> for DiskDisconnectReceipt {
1404 fn from(receipt: &StoredDisconnectReceipt) -> Self {
1405 Self {
1406 payload_digest: receipt.payload_digest.clone(),
1407 result: DiskDisconnectResult {
1408 durable_receipt: DiskDurableReceipt::from(&receipt.result.durable_receipt),
1409 provider_id: receipt.result.provider_id.clone(),
1410 disconnected: receipt.result.disconnected,
1411 },
1412 }
1413 }
1414}
1415
1416impl From<DiskDisconnectReceipt> for StoredDisconnectReceipt {
1417 fn from(receipt: DiskDisconnectReceipt) -> Self {
1418 Self {
1419 payload_digest: receipt.payload_digest,
1420 result: DisconnectReceipt {
1421 durable_receipt: DurableProviderReceipt::from(receipt.result.durable_receipt),
1422 provider_id: receipt.result.provider_id,
1423 disconnected: receipt.result.disconnected,
1424 },
1425 }
1426 }
1427}
1428
1429impl From<&DurableProviderReceipt> for DiskDurableReceipt {
1430 fn from(receipt: &DurableProviderReceipt) -> Self {
1431 Self {
1432 receipt_id: receipt.receipt_id,
1433 store_revision: receipt.store_revision.clone(),
1434 provider_state_revision: receipt.provider_state_revision.clone(),
1435 committed_at: receipt.committed_at,
1436 }
1437 }
1438}
1439
1440impl From<DiskDurableReceipt> for DurableProviderReceipt {
1441 fn from(receipt: DiskDurableReceipt) -> Self {
1442 Self {
1443 receipt_id: receipt.receipt_id,
1444 store_revision: receipt.store_revision,
1445 provider_state_revision: receipt.provider_state_revision,
1446 committed_at: receipt.committed_at,
1447 }
1448 }
1449}
1450
1451impl From<&super::types::DurableConnectionDescriptor> for DiskConnectionDescriptor {
1452 fn from(connection: &super::types::DurableConnectionDescriptor) -> Self {
1453 Self {
1454 provider_id: connection.provider_id.clone(),
1455 setup_values: disk_setup_values(&connection.setup_values),
1456 setup_fingerprint: connection.setup_fingerprint.clone(),
1457 recipe_fingerprint: connection.recipe_fingerprint.clone(),
1458 auth_method: connection.auth_method.clone(),
1459 credential_fields: connection.credential_fields.clone(),
1460 connection_generation: connection.connection_generation,
1461 connected_at: connection.connected_at,
1462 }
1463 }
1464}
1465
1466impl From<DiskConnectionDescriptor> for super::types::DurableConnectionDescriptor {
1467 fn from(connection: DiskConnectionDescriptor) -> Self {
1468 Self {
1469 provider_id: connection.provider_id,
1470 setup_values: live_setup_values(connection.setup_values),
1471 setup_fingerprint: connection.setup_fingerprint,
1472 recipe_fingerprint: connection.recipe_fingerprint,
1473 auth_method: connection.auth_method,
1474 credential_fields: connection.credential_fields,
1475 connection_generation: connection.connection_generation,
1476 connected_at: connection.connected_at,
1477 }
1478 }
1479}
1480
1481#[cfg(test)]
1482mod tests {
1483 use std::sync::atomic::Ordering;
1484
1485 use super::*;
1486
1487 #[test]
1488 fn reload_if_changed_fast_path_skips_the_transaction_when_unchanged() {
1489 let temporary = tempfile::tempdir().expect("tempdir");
1490 let store = ProviderStore::open(temporary.path().join("providers")).expect("store");
1491 let unknown = ProviderStoreGeneration::new(999_999).expect("generation");
1492 let snapshot = store
1494 .reload_if_changed(unknown)
1495 .expect("first reload")
1496 .expect("fresh generation differs from the known one");
1497 assert_eq!(store.transaction_opens.load(Ordering::Relaxed), 1);
1498 assert!(
1500 store
1501 .reload_if_changed(snapshot.generation())
1502 .expect("second reload")
1503 .is_none()
1504 );
1505 assert_eq!(
1506 store.transaction_opens.load(Ordering::Relaxed),
1507 1,
1508 "unchanged store must skip the transaction"
1509 );
1510 }
1511
1512 #[test]
1513 fn reload_if_changed_reopens_after_external_modification() {
1514 let temporary = tempfile::tempdir().expect("tempdir");
1515 let store = ProviderStore::open(temporary.path().join("providers")).expect("store");
1516 let unknown = ProviderStoreGeneration::new(999_999).expect("generation");
1517 let snapshot = store
1518 .reload_if_changed(unknown)
1519 .expect("first reload")
1520 .expect("fresh generation differs from the known one");
1521 assert_eq!(store.transaction_opens.load(Ordering::Relaxed), 1);
1522 std::fs::write(
1525 temporary.path().join("providers").join(PROVIDER_STORE_FILE),
1526 b"external write",
1527 )
1528 .expect("external write");
1529 assert!(store.reload_if_changed(snapshot.generation()).is_err());
1530 assert_eq!(
1531 store.transaction_opens.load(Ordering::Relaxed),
1532 2,
1533 "modified store must reopen the transaction"
1534 );
1535 }
1536}