Skip to main content

file_engine/
error.rs

1use std::path::PathBuf;
2use thiserror::Error;
3
4#[derive(Debug, Error)]
5pub enum FileEngineError {
6    #[error("source not found: {0:?}")]
7    SourceNotFound(PathBuf),
8
9    #[error("destination already exists: {0:?}")]
10    DestinationExists(PathBuf),
11
12    #[error("operation cancelled")]
13    Cancelled,
14
15    #[error("insufficient disk space: needed {needed} bytes, available {available} bytes")]
16    InsufficientSpace { needed: u64, available: u64 },
17
18    #[error("permission denied: {0:?}")]
19    PermissionDenied(PathBuf),
20
21    #[error("io error on {path:?}: {source}")]
22    Io {
23        path: PathBuf,
24        #[source]
25        source: std::io::Error,
26    },
27
28    #[error("could not infer compression format from destination: {0:?}")]
29    UnknownCompressFormat(PathBuf),
30
31    #[error("gzip compression requires a single file, got a directory: {0:?}")]
32    GzipRequiresFile(PathBuf),
33}
34
35pub type Result<T> = std::result::Result<T, FileEngineError>;
36
37/// Maps a raw `io::Error` to the closest `FileEngineError` variant, used by
38/// every module that touches the filesystem directly.
39///
40/// `InsufficientSpace`'s `available` is always reported as `0`: there is no
41/// disk-space-query dependency in this crate (out of scope for this pass),
42/// so the only thing known for certain when `ErrorKind::StorageFull` occurs
43/// is that the write failed, not how much space actually exists.
44// Every feature that touches the filesystem uses this, but with every
45// filesystem-touching feature off (`--no-default-features`) nothing calls
46// it — `error.rs` itself has no feature gate (§5.1), so it's still compiled.
47#[allow(dead_code)]
48pub(crate) fn from_io(path: PathBuf, source: std::io::Error) -> FileEngineError {
49    match source.kind() {
50        std::io::ErrorKind::NotFound => FileEngineError::SourceNotFound(path),
51        std::io::ErrorKind::PermissionDenied => FileEngineError::PermissionDenied(path),
52        std::io::ErrorKind::StorageFull => FileEngineError::InsufficientSpace {
53            needed: 0,
54            available: 0,
55        },
56        _ => FileEngineError::Io { path, source },
57    }
58}
59
60#[cfg(feature = "diagnostics")]
61use error_engine::{EngineDiagnostic, Severity};
62
63#[cfg(feature = "diagnostics")]
64impl EngineDiagnostic for FileEngineError {
65    fn code(&self) -> &'static str {
66        match self {
67            Self::SourceNotFound(_) => "FE_SOURCE_NOT_FOUND",
68            Self::DestinationExists(_) => "FE_DEST_EXISTS",
69            Self::Cancelled => "FE_CANCELLED",
70            Self::InsufficientSpace { .. } => "FE_NO_SPACE",
71            Self::PermissionDenied(_) => "FE_PERMISSION_DENIED",
72            Self::Io { .. } => "FE_IO",
73            Self::UnknownCompressFormat(_) => "FE_UNKNOWN_COMPRESS_FORMAT",
74            Self::GzipRequiresFile(_) => "FE_GZIP_REQUIRES_FILE",
75        }
76    }
77
78    fn severity(&self) -> Severity {
79        Severity::Error
80    }
81
82    fn context(&self) -> Vec<(&'static str, String)> {
83        match self {
84            Self::SourceNotFound(p) => vec![("path", p.display().to_string())],
85            Self::DestinationExists(p) => vec![("path", p.display().to_string())],
86            Self::InsufficientSpace { needed, available } => vec![
87                ("needed", needed.to_string()),
88                ("available", available.to_string()),
89            ],
90            Self::PermissionDenied(p) => vec![("path", p.display().to_string())],
91            Self::Io { path, .. } => vec![("path", path.display().to_string())],
92            Self::Cancelled => vec![],
93            Self::UnknownCompressFormat(p) => vec![("path", p.display().to_string())],
94            Self::GzipRequiresFile(p) => vec![("path", p.display().to_string())],
95        }
96    }
97}
98
99/// file-engine's own diagnostic catalog, embedded at compile time.
100/// Consuming apps merge it into their own catalog:
101///
102/// ```ignore
103/// let catalog = error_engine::Catalog::load_or_fallback("errors.toml")
104///     .merged_with(file_engine::catalog());
105/// ```
106#[cfg(feature = "diagnostics")]
107pub fn catalog() -> error_engine::Catalog {
108    error_engine::Catalog::from_str(include_str!("../errors.toml"))
109        .expect("file-engine's own catalog is valid TOML — covered by tests")
110}