Skip to main content

file_engine/
error.rs

1use std::io;
2use std::path::PathBuf;
3
4use thiserror::Error;
5
6#[derive(Debug, Error)]
7pub enum Error {
8    #[error("source not found: {path}")]
9    SourceNotFound { path: PathBuf },
10
11    #[error("destination already exists: {path}")]
12    DestExists { path: PathBuf },
13
14    #[error("operation cancelled")]
15    Cancelled,
16
17    #[error("insufficient disk space: needed {needed} bytes, available {available} bytes")]
18    NoSpace { needed: u64, available: u64 },
19
20    #[error("permission denied: {path}")]
21    PermissionDenied { path: PathBuf },
22
23    #[error("io error on {path}: {source}")]
24    Io { path: PathBuf, source: io::Error },
25
26    #[cfg(feature = "compress")]
27    #[error("could not infer compression format from destination: {path}")]
28    UnknownCompressFormat { path: PathBuf },
29
30    #[cfg(feature = "analyze")]
31    #[error("invalid glob pattern {pattern:?}: {source}")]
32    InvalidGlobPattern {
33        pattern: String,
34        source: globset::Error,
35    },
36
37    #[cfg(feature = "compress")]
38    #[error("gzip compression requires a single file, got a directory: {path}")]
39    GzipRequiresFile { path: PathBuf },
40
41    // The four variants below back the pre-flight validation in
42    // `profiler::validate` — gated on `operations` since that's the
43    // only feature that ever constructs them, matching the
44    // `compress`-gated variants above.
45    #[cfg(feature = "operations")]
46    #[error("filename differs only by case from another entry, which the destination filesystem cannot represent: {path} collides with {other}")]
47    CaseCollision { path: PathBuf, other: PathBuf },
48
49    #[cfg(feature = "operations")]
50    #[error("file exceeds the destination filesystem's maximum file size: {path} ({size} bytes, max {max} bytes)")]
51    FileTooLargeForDest { path: PathBuf, size: u64, max: u64 },
52
53    #[cfg(feature = "operations")]
54    #[error("filename is reserved or invalid on the destination filesystem: {path}")]
55    ReservedName { path: PathBuf },
56
57    /// Unlike the three variants above (per-entry, governed by
58    /// `ErrorStrategy`), this describes a whole-destination risk, not a
59    /// property of any specific entry — `is_fatal` accordingly.
60    #[cfg(feature = "operations")]
61    #[error(
62        "destination filesystem ({filesystem}) has a known write-integrity issue on this platform"
63    )]
64    FilesystemIntegrityRisk { filesystem: String },
65
66    /// Whole-batch pre-flight validation for `MoveManyBuilder`, same
67    /// spirit as `FilesystemIntegrityRisk` above: two sources sharing a
68    /// basename is ambiguous ("moved into `dest`" would mean two
69    /// different things), so it's caught before any source is touched
70    /// rather than surfacing as a confusing overwrite of one by the
71    /// other partway through. `is_fatal` accordingly.
72    #[cfg(feature = "operations")]
73    #[error("two sources would both move to the same destination name: {path} and {other}")]
74    DuplicateSourceName { path: PathBuf, other: PathBuf },
75
76    /// `MoveManyBuilder` pre-flight validation: a source with no final
77    /// path component (`/`, `.`, `..`, ...) has nothing to name its
78    /// destination entry after. `is_fatal`, same reasoning as
79    /// `DuplicateSourceName`.
80    #[cfg(feature = "operations")]
81    #[error("source path has no file name to move under: {path}")]
82    InvalidSourceName { path: PathBuf },
83}
84
85impl Error {
86    /// Fatal errors stop all remaining dispatch regardless of the
87    /// configured `ErrorStrategy`; per-entry errors are handled per that
88    /// strategy. `PermissionDenied`/`Io` default to per-entry despite
89    /// being genuinely ambiguous.
90    ///
91    /// Only consumed by `operations`-gated code (`dispatcher.rs`,
92    /// `move_path.rs`, `sync.rs`, `compress.rs`) — gated to match, since
93    /// a `watch`-only build (which doesn't imply `operations`) would
94    /// otherwise leave this genuinely dead and fail the crate's
95    /// warnings-as-errors build (`.cargo/config.toml`).
96    #[cfg(feature = "operations")]
97    pub(crate) fn is_fatal(&self) -> bool {
98        matches!(
99            self,
100            Error::Cancelled
101                | Error::NoSpace { .. }
102                | Error::FilesystemIntegrityRisk { .. }
103                | Error::DuplicateSourceName { .. }
104                | Error::InvalidSourceName { .. }
105        )
106    }
107}
108
109/// Maps a raw `io::Error` onto the crate's `Error` variants — shared by
110/// every module that turns a filesystem call's `io::Error` into one.
111/// Previously duplicated ten times, nearly identically, across
112/// `operations`/`profiler`/`analysis` (see
113/// `dev-docs/design/error-classification-audit.md`'s "Research"
114/// section); consolidated here specifically because `error.rs` is the
115/// one module every feature combination compiles unconditionally —
116/// `profiler::scan`'s copy of this was gated behind `operations`, which
117/// is exactly why `analysis::util` couldn't reuse it and grew its own.
118///
119/// `needed` is only meaningful for the `StorageFull` arm (`NoSpace`'s
120/// `needed` field); callers with no real figure to hand pass `0` rather
121/// than fabricating one, matching every pre-consolidation call site
122/// except `planner::action`'s (which has the entry's real size).
123/// `available` isn't queried at this level either (would need an extra
124/// statvfs-style syscall) so it's always reported as `0`.
125///
126/// Gated on the union of every feature that actually calls this
127/// (`operations`, `watch`, `analyze` independently — none of the three
128/// implies another) rather than left unconditional, so a build enabling
129/// none of them (e.g. `--no-default-features --features diagnostics`,
130/// exercised by CI's feature-powerset check) doesn't fail on dead code.
131#[cfg(any(feature = "operations", feature = "watch", feature = "analyze"))]
132pub(crate) fn classify_io_error(err: io::Error, path: PathBuf, needed: u64) -> Error {
133    match err.kind() {
134        io::ErrorKind::NotFound => Error::SourceNotFound { path },
135        io::ErrorKind::PermissionDenied => Error::PermissionDenied { path },
136        io::ErrorKind::StorageFull => Error::NoSpace {
137            needed,
138            available: 0,
139        },
140        _ => Error::Io { path, source: err },
141    }
142}
143
144pub type Result<T> = std::result::Result<T, Error>;