Skip to main content

coven_sync/
file_error.rs

1//! Typed file read and upload failures shared with the application facade.
2
3use coven_database::{DbError, LocalFileError};
4use coven_foundation::{
5    files::{FileError, StoreLockError},
6    id_source::{DeviceId, FileId},
7};
8use coven_storage::StorageError;
9use std::path::PathBuf;
10
11/// Reading a file failed with a cause the app can distinguish (E8).
12#[derive(Debug, thiserror::Error)]
13pub enum FileReadError {
14    /// This file uses a newer coven format (Appendix C.2).
15    #[error("an update is required to read this file")]
16    UpdateRequired(crate::RequiredUpdate),
17    /// No storage capability is connected.
18    #[error("no storage connected")]
19    NoStorage,
20    /// Storage confirms nothing at the path a stored file's record names:
21    /// the provider lost it (§17.6). Nothing brings it back on its own.
22    #[error("file {id}, uploaded by device {device:?}, is missing from storage")]
23    Missing {
24        /// The file's id.
25        id: String,
26        /// The device that uploaded it.
27        device: DeviceId,
28    },
29    /// The file is only on one device, another one or this one with no
30    /// source to read: nothing here can read it, and nothing waits for it
31    /// (§17.2).
32    #[error("file {file} is only on device {device:?}")]
33    DeviceOnly {
34        /// The file.
35        file: FileId,
36        /// The device that has it.
37        device: DeviceId,
38    },
39    /// A recorded original is absent.
40    #[error("file {id} is missing at {}", path.display())]
41    UserFileMissing {
42        /// The file's id.
43        id: String,
44        /// Recorded original path.
45        path: PathBuf,
46    },
47    /// An original changed after it was staged.
48    #[error("file {id} changed at {}", path.display())]
49    UserFileChanged {
50        /// The file's id.
51        id: String,
52        /// Recorded original path.
53        path: PathBuf,
54    },
55    /// Authentication, a chunk hash or the whole-file hash failed.
56    #[error("file {id} failed its content check")]
57    Integrity {
58        /// The file's id.
59        id: String,
60        /// The authentication failure, when a chunk failed to open. A size,
61        /// layout or hash disagreement has no cause beyond the comparison.
62        #[source]
63        cause: Option<std::sync::Arc<coven_crypto::CryptoError>>,
64    },
65    /// The requested range is outside the file.
66    #[error("file {id} range {offset}..{end} exceeds {size}")]
67    RangeOutOfBounds {
68        /// The file's id.
69        id: String,
70        /// Requested start.
71        offset: u64,
72        /// Requested end, saturated on overflow.
73        end: u64,
74        /// Whole plaintext size.
75        size: u64,
76    },
77    /// A database failure, retaining its cause.
78    #[error(transparent)]
79    Database(DbError),
80    /// A disk failure, retaining its cause.
81    #[error(transparent)]
82    Disk(FileError),
83    /// The store cannot be retained while opening its file.
84    #[error(transparent)]
85    Lock(StoreLockError),
86    /// A provider failure, retaining its classification and native cause.
87    #[error(transparent)]
88    Storage(StorageError),
89    /// Recording what a read or an upload settled failed in sync's own
90    /// work, as a retry wait the clock can't hold, keeping its cause; a
91    /// failed read whose failure couldn't be recorded keeps both (§17.9).
92    #[error(transparent)]
93    Sync(Box<crate::SyncError>),
94    /// The read's work panicked: a bug, which the caller reports (E15).
95    #[error(transparent)]
96    Bug(#[from] coven_foundation::task::Bug),
97}
98impl From<FileError> for FileReadError {
99    fn from(error: FileError) -> Self {
100        match error {
101            FileError::Bug(bug) => Self::Bug(bug),
102            error => Self::Disk(error),
103        }
104    }
105}
106impl From<StoreLockError> for FileReadError {
107    fn from(error: StoreLockError) -> Self {
108        match error {
109            StoreLockError::Bug(bug) => Self::Bug(bug),
110            error => Self::Lock(error),
111        }
112    }
113}
114impl From<StorageError> for FileReadError {
115    fn from(error: StorageError) -> Self {
116        match error {
117            StorageError::Bug(bug) => Self::Bug(bug),
118            error => Self::Storage(error),
119        }
120    }
121}
122impl coven_foundation::task::CarriesBug for FileReadError {
123    fn bug(&self) -> Option<&coven_foundation::task::Bug> {
124        match self {
125            Self::Bug(bug) => Some(bug),
126            Self::Database(error) => error.bug(),
127            Self::Disk(error) => error.bug(),
128            Self::Lock(error) => error.bug(),
129            Self::Storage(error) => error.bug(),
130            Self::Sync(error) => error.bug(),
131            _ => None,
132        }
133    }
134}
135impl From<coven_foundation::files::WriterError> for FileReadError {
136    fn from(error: coven_foundation::files::WriterError) -> Self {
137        match error {
138            coven_foundation::files::WriterError::Lock(error) => error.into(),
139            coven_foundation::files::WriterError::File(error) => error.into(),
140        }
141    }
142}
143
144/// A database or provider failure as itself; any other of sync's own
145/// failures kept whole.
146impl From<crate::SyncError> for FileReadError {
147    fn from(error: crate::SyncError) -> Self {
148        match error {
149            crate::SyncError::Database(error) => error.into(),
150            crate::SyncError::Storage(error) => error.into(),
151            crate::SyncError::Bug(bug) => Self::Bug(bug),
152            error => Self::Sync(Box::new(error)),
153        }
154    }
155}
156impl From<DbError> for FileReadError {
157    fn from(error: DbError) -> Self {
158        match error {
159            DbError::Disk(error) => Self::Disk(error),
160            DbError::Lock(error) => Self::Lock(error),
161            DbError::Bug(bug) => Self::Bug(bug),
162            error => Self::Database(error),
163        }
164    }
165}
166impl FileReadError {
167    /// A chunk of file `id` that did not seal or open. An authentication
168    /// failure keeps its crypto error. A chunk length outside the header's
169    /// layout is the layout's disagreement, which the comparison states.
170    pub(crate) fn chunk(id: String, error: coven_format::file::FileError) -> Self {
171        Self::Integrity {
172            id,
173            cause: match error {
174                coven_format::file::FileError::Crypto(error) => Some(std::sync::Arc::new(error)),
175                coven_format::file::FileError::Format(_) => None,
176            },
177        }
178    }
179}
180impl From<LocalFileError> for FileReadError {
181    fn from(error: LocalFileError) -> Self {
182        match error {
183            LocalFileError::UserFileMissing { id, path } => Self::UserFileMissing { id, path },
184            LocalFileError::UserFileChanged { id, path } => Self::UserFileChanged { id, path },
185            LocalFileError::Integrity { id } => Self::Integrity { id, cause: None },
186            LocalFileError::RangeOutOfBounds { id, offset, end, size } => {
187                Self::RangeOutOfBounds { id, offset, end, size }
188            }
189            LocalFileError::Database(error) => error.into(),
190            LocalFileError::Disk(error) => Self::Disk(error),
191            LocalFileError::Lock(error) => Self::Lock(error),
192            LocalFileError::Bug(bug) => Self::Bug(bug),
193        }
194    }
195}
196
197/// A file transfer retains its typed cause and exact refusal. The pending
198/// record keeps the category before the app-facing error merges content checks.
199#[derive(Debug, thiserror::Error)]
200pub(crate) enum TransferError {
201    /// Content or immutable identity verification failed.
202    #[error("file transfer was refused: {0:?}")]
203    Refused(crate::Refusal),
204    /// Reading, storing or checking bytes failed.
205    #[error(transparent)]
206    File(#[from] FileReadError),
207}
208
209impl coven_foundation::task::CarriesBug for TransferError {
210    fn bug(&self) -> Option<&coven_foundation::task::Bug> {
211        match self {
212            Self::Refused(_) => None,
213            Self::File(error) => error.bug(),
214        }
215    }
216}
217
218impl From<StorageError> for TransferError {
219    fn from(error: StorageError) -> Self {
220        Self::File(error.into())
221    }
222}
223impl From<DbError> for TransferError {
224    fn from(error: DbError) -> Self {
225        Self::File(error.into())
226    }
227}
228impl From<FileError> for TransferError {
229    fn from(error: FileError) -> Self {
230        Self::File(error.into())
231    }
232}
233impl From<LocalFileError> for TransferError {
234    fn from(error: LocalFileError) -> Self {
235        Self::File(error.into())
236    }
237}
238
239#[cfg(test)]
240#[path = "file_error_tests.rs"]
241mod tests;