Skip to main content

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;