1use std::num::NonZeroU64;
8use std::path::Path;
9
10use serde::{Deserialize, Deserializer, Serialize};
11
12use crate::membership::AuthorHead;
13use crate::store_commit::{ObjectHash, StoreDeviceRegistration, StoreProtocolError};
14
15#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
21#[serde(transparent)]
22pub struct ExactObjectVersion(String);
23
24impl ExactObjectVersion {
25 pub fn from_provider(value: String) -> Result<Self, StorageError> {
26 if value.is_empty() {
27 return Err(StorageError::Configuration(
28 "cloud object version token is empty".to_string(),
29 ));
30 }
31 Ok(Self(value))
32 }
33
34 pub fn as_provider(&self) -> &str {
35 &self.0
36 }
37}
38
39impl<'de> Deserialize<'de> for ExactObjectVersion {
40 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
41 where
42 D: Deserializer<'de>,
43 {
44 let value = String::deserialize(deserializer)?;
45 if value.is_empty() {
46 return Err(serde::de::Error::custom(
47 "cloud object version token is empty",
48 ));
49 }
50 Ok(Self(value))
51 }
52}
53
54mod domains;
55mod provider_binding;
56mod rotation;
57
58pub use domains::{
59 CircleProtocolObjectDomain, ProtectedObjectDomain, ProtocolObjectDomain,
60 RecipientSealedProtocolObjectDomain, SignedStoreProtocolObjectDomain,
61 StoreEncryptedProtocolObjectDomain,
62};
63pub use provider_binding::*;
64#[cfg(any(test, feature = "test-utils"))]
65pub use rotation::LocalRotation;
66pub use rotation::RotationPending;
67#[cfg(any(test, feature = "test-utils"))]
68pub use rotation::RotationPendingState;
69pub use rotation::{RotationGate, RotationGateError, ROTATION_GATE_STATE_KEY};
70
71pub struct ProtocolObjectContext {
98 store_root_hash: ObjectHash,
99 domain: ProtectedObjectDomain,
100 protection: ProtocolObjectProtection,
101}
102
103#[derive(Clone)]
104pub enum ProtocolObjectProtection {
105 StoreEncrypted,
106 SignedPlaintext,
107 Circle(coven_keys::encryption::EncryptionService),
108 RecipientSealed,
109}
110
111impl ProtocolObjectContext {
112 pub fn store_encrypted(
113 store_root_hash: ObjectHash,
114 domain: StoreEncryptedProtocolObjectDomain,
115 ) -> Self {
116 Self {
117 store_root_hash,
118 domain: domain.0,
119 protection: ProtocolObjectProtection::StoreEncrypted,
120 }
121 }
122
123 pub fn signed_plaintext(
124 store_root_hash: ObjectHash,
125 domain: SignedStoreProtocolObjectDomain,
126 ) -> Self {
127 Self {
128 store_root_hash,
129 domain: domain.0,
130 protection: ProtocolObjectProtection::SignedPlaintext,
131 }
132 }
133
134 pub fn circle(
135 store_root_hash: ObjectHash,
136 domain: CircleProtocolObjectDomain,
137 encryption: coven_keys::encryption::EncryptionService,
138 ) -> Self {
139 Self {
140 store_root_hash,
141 domain: domain.0,
142 protection: ProtocolObjectProtection::Circle(encryption),
143 }
144 }
145
146 pub fn recipient_sealed(
147 store_root_hash: ObjectHash,
148 domain: RecipientSealedProtocolObjectDomain,
149 ) -> Self {
150 Self {
151 store_root_hash,
152 domain: domain.0,
153 protection: ProtocolObjectProtection::RecipientSealed,
154 }
155 }
156
157 pub fn store_root_hash(&self) -> ObjectHash {
158 self.store_root_hash
159 }
160
161 pub fn domain(&self) -> ProtectedObjectDomain {
162 self.domain
163 }
164
165 pub fn protection(&self) -> &ProtocolObjectProtection {
166 &self.protection
167 }
168
169 pub fn validate_path(&self, semantic_prefix: &str) -> Result<(), StorageError> {
170 let metadata = self.domain.metadata();
171 if semantic_prefix.contains("/copies/") || !metadata.path.accepts(semantic_prefix) {
172 return Err(StorageError::Parse(format!(
173 "object domain {:?} does not accept semantic path {semantic_prefix:?}",
174 self.domain
175 )));
176 }
177 Ok(())
178 }
179
180 pub fn validate_extension(&self, extension: &str) -> Result<(), StorageError> {
181 if extension != self.domain.extension() {
182 return Err(StorageError::Parse(format!(
183 "object domain {:?} does not accept extension {extension:?}",
184 self.domain
185 )));
186 }
187 Ok(())
188 }
189
190 pub fn validate_reference(
191 &self,
192 object: &ExactObjectRef,
193 semantic_prefix: &str,
194 ) -> Result<(), StorageError> {
195 self.validate_slot(object.slot(), semantic_prefix)
196 }
197
198 pub fn semantic_prefix_of<'slot>(&self, slot: &'slot ObjectSlot) -> Option<&'slot str> {
207 let semantic_prefix = slot.logical_key().strip_suffix(self.domain.extension())?;
208 self.validate_slot(slot, semantic_prefix)
209 .ok()
210 .map(|()| semantic_prefix)
211 }
212
213 pub fn validate_slot(
214 &self,
215 slot: &ObjectSlot,
216 semantic_prefix: &str,
217 ) -> Result<(), StorageError> {
218 self.validate_path(semantic_prefix)?;
219 let expected = format!("{semantic_prefix}{}", self.domain.extension());
220 if slot.logical_key() != expected {
221 return Err(StorageError::Parse(format!(
222 "protocol object {:?} does not match semantic path {semantic_prefix:?}",
223 slot.logical_key()
224 )));
225 }
226 Ok(())
227 }
228}
229
230#[derive(Clone)]
232pub enum BlobSpoolProtection {
233 Opaque(coven_keys::encryption::EncryptionService),
234 Browsable,
235}
236
237#[derive(Clone, Copy, Debug, PartialEq, Eq)]
238pub enum BlobSpoolWrite {
239 Created,
240 Reused,
241}
242
243#[derive(Clone, Copy)]
244pub struct BlobWriteAuthority<'a> {
245 pub reference: &'a crate::store_commit::StoreDeviceRegistrationRef,
246 pub registration: &'a crate::store_commit::StoreDeviceRegistration,
247}
248
249impl<'a> BlobWriteAuthority<'a> {
250 pub fn new(registration: &'a crate::store_commit::ReferencedStoreDeviceRegistration) -> Self {
251 Self {
252 reference: registration.reference(),
253 registration: registration.value(),
254 }
255 }
256}
257
258#[derive(
260 Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
261)]
262#[serde(deny_unknown_fields)]
263pub struct ExactObjectRef {
264 slot: ObjectSlot,
265 stored_size: u64,
266 stored_hash: ObjectHash,
267}
268
269impl ExactObjectRef {
270 pub fn new(slot: ObjectSlot, stored_size: u64, stored_hash: ObjectHash) -> Self {
271 Self {
272 slot,
273 stored_size,
274 stored_hash,
275 }
276 }
277
278 pub fn slot(&self) -> &ObjectSlot {
279 &self.slot
280 }
281
282 pub fn stored_size(&self) -> u64 {
283 self.stored_size
284 }
285
286 pub fn stored_hash(&self) -> ObjectHash {
287 self.stored_hash
288 }
289
290 pub fn verify(&self, bytes: &[u8]) -> Result<(), StorageError> {
291 if bytes.len() as u64 != self.stored_size || ObjectHash::digest(bytes) != self.stored_hash {
292 return Err(StorageError::InvalidContent(format!(
293 "exact object {} does not match stored size/hash",
294 self.slot.logical_key()
295 )));
296 }
297 Ok(())
298 }
299
300 pub fn verify_stored_facts(
304 &self,
305 path: &Path,
306 size: u64,
307 hash: ObjectHash,
308 ) -> Result<(), StorageError> {
309 if size != self.stored_size || hash != self.stored_hash {
310 return Err(StorageError::InvalidContent(format!(
311 "exact file {} does not match stored identity for {}",
312 path.display(),
313 self.slot.logical_key()
314 )));
315 }
316 Ok(())
317 }
318}
319
320#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
322#[serde(deny_unknown_fields)]
323pub struct PreparedExactObject {
324 reference: ExactObjectRef,
325 stored_bytes: Vec<u8>,
326}
327
328impl<'de> serde::Deserialize<'de> for PreparedExactObject {
329 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
330 where
331 D: serde::Deserializer<'de>,
332 {
333 #[derive(serde::Deserialize)]
334 #[serde(deny_unknown_fields)]
335 struct Fields {
336 reference: ExactObjectRef,
337 stored_bytes: Vec<u8>,
338 }
339
340 let fields = Fields::deserialize(deserializer)?;
341 Self::new(fields.reference, fields.stored_bytes).map_err(serde::de::Error::custom)
342 }
343}
344
345impl PreparedExactObject {
346 pub fn new(reference: ExactObjectRef, stored_bytes: Vec<u8>) -> Result<Self, StorageError> {
347 reference.verify(&stored_bytes)?;
348 Ok(Self {
349 reference,
350 stored_bytes,
351 })
352 }
353
354 pub fn reference(&self) -> &ExactObjectRef {
355 &self.reference
356 }
357
358 pub fn stored_bytes(&self) -> &[u8] {
359 &self.stored_bytes
360 }
361}
362
363#[derive(Clone, Copy, Debug, PartialEq, Eq)]
365pub enum StorageBackendFailure {
366 Authentication,
367 PermissionDenied,
368 ContainerNotFound,
369 RegionMismatch,
370 QuotaExceeded,
371 Configuration,
372 Transport,
373 Internal,
374}
375
376#[derive(Debug, thiserror::Error)]
377pub enum StorageError {
378 #[error("storage operation failed: {0}")]
379 Storage(String),
380 #[error("storage backend {kind:?} failure while {operation}: {source}")]
381 Backend {
382 kind: StorageBackendFailure,
383 operation: String,
384 #[source]
385 source: Box<dyn std::error::Error + Send + Sync>,
386 },
387 #[error("{operation}; storage cleanup failed: {cleanup}")]
388 CleanupFailed {
389 #[source]
390 operation: Box<StorageError>,
391 cleanup: Box<StorageError>,
392 },
393 #[error("{operation}; exact response settlement failed: {settlement}")]
394 UnresolvedOutcome {
395 #[source]
396 operation: Box<StorageError>,
397 settlement: Box<StorageError>,
398 },
399 #[error("storage configuration is invalid: {0}")]
400 Configuration(String),
401 #[error("storage object parse failed: {0}")]
402 Parse(String),
403 #[error("storage object JSON failed: {0}")]
404 Json(#[from] serde_json::Error),
405 #[error("storage key custody failed: {0}")]
406 Key(#[from] coven_keys::keys::KeyError),
407 #[error("provider probe journal is invalid: {0}")]
408 ProviderProbeJournal(#[from] crate::provider::ProviderProbeJournalError),
409 #[error("Store protocol object is invalid: {0}")]
410 StoreProtocol(#[source] Box<crate::store_commit::StoreProtocolError>),
411 #[error("stored blob reference is invalid: {0}")]
412 BlobLocator(#[from] crate::blob::locator::BlobLocatorError),
413 #[error("storage worker failed while {operation}: {source}")]
414 Blocking {
415 operation: &'static str,
416 #[source]
417 source: coven_foundation::blocking::BlockingTaskError,
418 },
419 #[error("storage URL is invalid: {0}")]
420 Url(#[from] url::ParseError),
421 #[error("object not found: {0}")]
422 NotFound(String),
423 #[error("storage object already exists: {0}")]
424 AlreadyExists(String),
425 #[error("reserved storage slot contains different bytes: {0}")]
426 SlotCollision(String),
427 #[error("prepared exact object differs from its durable bytes: {0}")]
430 PreparedObjectMismatch(String),
431 #[error("decryption failed for {context}: {source}")]
432 Decryption {
433 context: String,
434 #[source]
435 source: coven_keys::encryption::EncryptionError,
436 },
437 #[error("remote blob content is invalid: {0}")]
438 InvalidContent(String),
439 #[error("local blob filesystem failed: {0}")]
440 LocalFilesystem(#[from] coven_foundation::atomic_file::FileError),
441 #[error("storage I/O failed: {0}")]
442 Io(#[from] std::io::Error),
443 #[error("publishing a new local file failed: {0}")]
444 CommitNewFile(#[from] coven_foundation::local_file::CommitNewFileError),
445 #[error("unsafe blob path: {0}")]
446 UnsafeBlobPath(#[from] coven_foundation::store_dir::PathTokenError),
447 #[error("{0}")]
450 RotationPending(#[from] RotationPending),
451}
452
453impl StorageError {
454 pub fn backend(
455 kind: StorageBackendFailure,
456 operation: impl Into<String>,
457 source: impl std::error::Error + Send + Sync + 'static,
458 ) -> Self {
459 Self::Backend {
460 kind,
461 operation: operation.into(),
462 source: Box::new(source),
463 }
464 }
465
466 pub fn is_transport(&self) -> bool {
467 match self {
468 Self::Storage(_)
469 | Self::Backend {
470 kind: StorageBackendFailure::Transport,
471 ..
472 } => true,
473 Self::CleanupFailed { operation, .. } | Self::UnresolvedOutcome { operation, .. } => {
474 operation.is_transport()
475 }
476 _ => false,
477 }
478 }
479
480 pub fn backend_failure(&self) -> Option<StorageBackendFailure> {
481 match self {
482 Self::Storage(_) => Some(StorageBackendFailure::Transport),
483 Self::Backend { kind, .. } => Some(*kind),
484 Self::CleanupFailed { operation, .. } | Self::UnresolvedOutcome { operation, .. } => {
485 operation.backend_failure()
486 }
487 Self::Configuration(_) => Some(StorageBackendFailure::Configuration),
488 _ => None,
489 }
490 }
491
492 pub fn cleanup_causes(&self) -> Option<(&StorageError, &StorageError)> {
493 match self {
494 Self::CleanupFailed { operation, cleanup } => Some((operation, cleanup)),
495 _ => None,
496 }
497 }
498}
499
500impl From<crate::store_commit::StoreProtocolError> for StorageError {
501 fn from(source: crate::store_commit::StoreProtocolError) -> Self {
502 Self::StoreProtocol(Box::new(source))
503 }
504}
505
506#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
508#[serde(
509 tag = "kind",
510 content = "value",
511 rename_all = "snake_case",
512 deny_unknown_fields
513)]
514pub enum PhysicalObjectLocator {
515 LogicalKey,
516 Opaque(String),
517}
518
519#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
521#[serde(deny_unknown_fields)]
522pub struct ObjectSlot {
523 logical_key: String,
524 physical: PhysicalObjectLocator,
525}
526
527impl<'de> Deserialize<'de> for ObjectSlot {
528 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
529 where
530 D: Deserializer<'de>,
531 {
532 #[derive(Deserialize)]
533 #[serde(deny_unknown_fields)]
534 struct Fields {
535 logical_key: String,
536 physical: PhysicalObjectLocator,
537 }
538
539 let fields = Fields::deserialize(deserializer)?;
540 Self::new(fields.logical_key, fields.physical).map_err(serde::de::Error::custom)
541 }
542}
543
544impl ObjectSlot {
545 pub fn logical(logical_key: String) -> Result<Self, StorageError> {
546 Self::new(logical_key, PhysicalObjectLocator::LogicalKey)
547 }
548
549 pub fn opaque(logical_key: String, provider_id: String) -> Result<Self, StorageError> {
550 Self::new(logical_key, PhysicalObjectLocator::Opaque(provider_id))
551 }
552
553 fn new(logical_key: String, physical: PhysicalObjectLocator) -> Result<Self, StorageError> {
554 let slot = Self {
555 logical_key,
556 physical,
557 };
558 slot.validate()?;
559 Ok(slot)
560 }
561
562 pub fn validate(&self) -> Result<(), StorageError> {
563 if self.logical_key.is_empty() {
564 return Err(StorageError::Configuration(
565 "object slot logical key is empty".to_string(),
566 ));
567 }
568 if matches!(&self.physical, PhysicalObjectLocator::Opaque(value) if value.is_empty()) {
569 return Err(StorageError::Configuration(
570 "object slot provider locator is empty".to_string(),
571 ));
572 }
573 Ok(())
574 }
575
576 pub fn logical_key(&self) -> &str {
577 &self.logical_key
578 }
579
580 pub fn physical(&self) -> &PhysicalObjectLocator {
581 &self.physical
582 }
583
584 pub fn require_logical_key_for(&self, provider: &str) -> Result<(), StorageError> {
587 self.validate()?;
588 if self.physical != PhysicalObjectLocator::LogicalKey {
589 return Err(StorageError::Configuration(format!(
590 "{provider} slot for {} must use its logical key",
591 self.logical_key
592 )));
593 }
594 Ok(())
595 }
596}
597
598#[derive(Clone, Debug)]
599pub struct VerifiedObject<T> {
600 pub value: T,
601 pub bytes: Vec<u8>,
602 pub semantic_hash: ObjectHash,
603 pub object: ExactObjectRef,
604}
605
606#[derive(Debug, thiserror::Error)]
607pub enum StoreObjectError {
608 #[error("{0}")]
609 Storage(
610 #[from]
611 #[source]
612 StorageError,
613 ),
614 #[error("Store object {key:?} is invalid for semantic object {semantic_prefix:?}: {source}")]
615 InvalidObject {
616 semantic_prefix: String,
617 key: String,
618 #[source]
619 source: Box<StoreProtocolError>,
620 },
621}
622
623pub fn decode_protocol_object<T: serde::de::DeserializeOwned>(
626 bytes: &[u8],
627) -> Result<T, StoreProtocolError> {
628 serde_json::from_slice(bytes).map_err(StoreProtocolError::from)
629}
630
631pub fn verify_store_root(
634 expected: ObjectHash,
635 actual: ObjectHash,
636) -> Result<(), StoreProtocolError> {
637 if actual != expected {
638 return Err(StoreProtocolError::StoreRootMismatch { expected, actual });
639 }
640 Ok(())
641}
642
643pub fn verify_membership_head_reference(
644 head: &AuthorHead,
645 expected_coord: &crate::membership::MembershipCoord,
646 expected_head_hash: ObjectHash,
647 registration: &StoreDeviceRegistration,
648) -> Result<(), StoreProtocolError> {
649 if head.entry_coord() != *expected_coord
650 || head.head_hash() != expected_head_hash
651 || registration.author_pubkey != expected_coord.author_pubkey
652 || !head.verify(registration)
653 {
654 return Err(StoreProtocolError::Malformed(
655 "exact membership head differs from its reference or certified author".to_string(),
656 ));
657 }
658 Ok(())
659}
660
661#[derive(Debug, Clone)]
669pub struct ExactProtocolObject<T> {
670 pub value: T,
671 pub bytes: Vec<u8>,
672 pub prepared: PreparedExactObject,
673}
674
675pub struct PreparedProtocolObject<T> {
676 pub value: T,
677 pub prepared: PreparedExactObject,
678}
679
680#[cfg(test)]
681mod tests;