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>;