coven_storage/object_metadata.rs
1//! Provider observations identify replacements independently of timestamps and checksums.
2
3use crate::{ByteRange, ObjectPath, StorageError, StorageFailure};
4use coven_format::value::StorageTime;
5use std::time::Duration;
6
7/// Metadata of an object returned by a provider read, exact status or listing.
8#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
9pub struct StoredObject {
10 /// Validated path relative to the store's location.
11 pub path: ObjectPath,
12 /// Complete encrypted length, including for ranged reads.
13 pub size: u64,
14 /// Provider publication time for immutable objects, replacement time otherwise.
15 /// OneDrive uses the copy's lastModifiedDateTime. AWS multipart Last-Modified
16 /// still reports initiation; its live check exposes that unmet guarantee.
17 pub stored_at: StorageTime,
18 /// Timestamp resolution. S3 uses its whole-second HEAD/GET representation
19 /// for listings too; other providers retain their returned fractional digits.
20 pub time_resolution: Duration,
21 /// Native object id, or the complete key on S3.
22 pub object_id: String,
23 /// Opaque replacement identity, never a proof of byte equality. S3 immutable
24 /// listings use the ETag; replaceable paths use exact status to obtain the
25 /// generation or Coven's atomically stored replacement token.
26 pub revision: String,
27 /// Opaque native condition from the same observation: ETag on S3/OneDrive,
28 /// generation on GCS, revision on Dropbox, record change tag on CloudKit.
29 /// This is separate from revision identity: an S3 version is not an ETag.
30 pub replace_token: String,
31 /// Provider checksum, when returned. ETags are never inferred to be checksums.
32 pub checksum: Option<ObjectChecksum>,
33}
34
35impl StoredObject {
36 pub(crate) fn read_length(
37 &self,
38 path: &ObjectPath,
39 range: Option<ByteRange>,
40 ) -> Result<u64, StorageError> {
41 self.check(path)?;
42 match range {
43 Some(range) if range.end() > self.size => Err(StorageFailure::InvalidRange.into()),
44 Some(range) => Ok(range.len()),
45 None => Ok(self.size),
46 }
47 }
48
49 pub(crate) fn check(&self, path: &ObjectPath) -> Result<(), StorageError> {
50 if &self.path != path
51 || self.object_id.is_empty()
52 || self.revision.is_empty()
53 || self.replace_token.is_empty()
54 || self.time_resolution.is_zero()
55 {
56 return Err(StorageFailure::Protocol.with_source("invalid object metadata"));
57 }
58 Ok(())
59 }
60}
61
62/// Encrypted bytes and the provider observation identifying those exact bytes.
63#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct ObjectRead {
65 /// Metadata describes the complete object, even when only a range was read.
66 pub metadata: StoredObject,
67 /// The requested complete body or byte range.
68 pub bytes: Vec<u8>,
69}
70
71impl ObjectRead {
72 pub(crate) fn check(
73 &self,
74 path: &ObjectPath,
75 range: Option<ByteRange>,
76 ) -> Result<(), StorageError> {
77 let expected = self.metadata.read_length(path, range)?;
78 if self.bytes.len() as u64 != expected {
79 return Err(
80 StorageFailure::Protocol.with_source("read length disagrees with object metadata")
81 );
82 }
83 Ok(())
84 }
85}
86
87/// The algorithm and native encoding of a provider checksum.
88#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
89pub enum ChecksumValue {
90 /// Hexadecimal SHA-1 of the complete content (OneDrive).
91 Sha1Hex(String),
92 /// Dropbox's hexadecimal SHA-256 of concatenated 4 MiB block hashes.
93 DropboxContentHash(String),
94 /// Base64 QuickXorHash (OneDrive), a provider checksum, not authentication.
95 QuickXorBase64(String),
96 /// Base64 SHA-256 (S3); its scope determines whether it hashes parts or content.
97 Sha256Base64(String),
98 /// Base64 CRC64NVME (S3).
99 Crc64NvmeBase64(String),
100 /// Base64 CRC32C (S3).
101 Crc32cBase64(String),
102 /// Base64 CRC32 (S3).
103 Crc32Base64(String),
104 /// Base64 SHA-1 (S3).
105 Sha1Base64(String),
106}
107
108/// Full-content checksums and multipart composites have different comparison rules.
109#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
110pub enum ObjectChecksum {
111 /// Comparable with the named algorithm run over the complete encrypted object.
112 FullObject(ChecksumValue),
113 /// Derived from multipart part checksums; cannot confirm a whole-object digest.
114 Composite(ChecksumValue),
115}
116
117#[cfg(test)]
118#[path = "object_metadata_tests.rs"]
119mod tests;