Skip to main content

mount/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Error types for the mount crate.
3//!
4//! Mount-side errors map cleanly to libc errno codes so the FUSE
5//! shell can hand them back to the kernel without a translation
6//! layer of its own. The mapping lives in [`MountError::to_errno`].
7
8use objects::error::HeddleError;
9
10/// Result alias used throughout the mount crate.
11pub type Result<T> = std::result::Result<T, MountError>;
12
13/// Errors surfaced by the content-addressed mount core.
14#[derive(Debug, thiserror::Error)]
15pub enum MountError {
16    /// The requested path or node does not exist in the current state.
17    #[error("not found: {0}")]
18    NotFound(String),
19
20    /// The node referenced by the caller is no longer valid (stale
21    /// inode, invalidated cache, etc).
22    #[error("stale node: {0}")]
23    Stale(String),
24
25    /// A path component traversed something that wasn't a directory.
26    #[error("not a directory: {0}")]
27    NotADirectory(String),
28
29    /// The thread name does not resolve to a current state.
30    #[error("thread {0} has no current state")]
31    UnknownThread(String),
32
33    /// Read-only filesystem (used while overlay-write is stubbed).
34    #[error("read-only filesystem")]
35    ReadOnly,
36
37    /// An entry with this name already exists (e.g. `O_CREAT|O_EXCL`
38    /// against an existing file, or `mkdir` against an existing dir).
39    /// Maps to `EEXIST` so userspace tooling that exercises atomic
40    /// "create-or-skip" semantics (cargo's lockfile lease, git's
41    /// `objects/<n>/<n>.tmp` placement) sees the conventional errno.
42    #[error("already exists: {0}")]
43    AlreadyExists(String),
44
45    /// Tried to operate on a file as if it were a directory
46    /// (e.g. `unlink` against a path that resolves to a directory).
47    /// Maps to `EISDIR`.
48    #[error("is a directory: {0}")]
49    IsADirectory(String),
50
51    /// Tried to `rmdir` a directory that still has visible children
52    /// (across the captured tree + pending overlay). Maps to
53    /// `ENOTEMPTY`.
54    #[error("directory not empty: {0}")]
55    NotEmpty(String),
56
57    /// Invalid argument from the caller (e.g. a name containing
58    /// `/`, `\0`, or `.`/`..`). Maps to `EINVAL`.
59    #[error("invalid argument: {0}")]
60    InvalidArgument(String),
61
62    /// A write or truncate would grow the hot buffer past the mount's
63    /// configured maximum file size. Maps to `EFBIG`.
64    #[error("file too large: {0}")]
65    FileTooLarge(String),
66
67    /// The platform shell failed to construct its mount session
68    /// (e.g. the Swift FSKit shim returned a null session handle).
69    /// Maps to `EIO`: the mount never came up, nothing to retry
70    /// at the filesystem layer.
71    #[error("mount session initialization failed: {0}")]
72    SessionInit(String),
73
74    /// Errors bubbling up from the underlying object store / repo.
75    #[error(transparent)]
76    Store(#[from] HeddleError),
77}
78
79impl MountError {
80    /// Translate this error into a libc errno suitable for handing
81    /// back to FUSE. Only the platform shell uses this — keeping it
82    /// here means platform code stays one-liners.
83    ///
84    /// `ESTALE` is POSIX-only; on Windows `libc` doesn't define it,
85    /// so the Windows build uses the POSIX value (`116`) verbatim.
86    /// The ProjFS shell translates this back into a Win32
87    /// `ERROR_FILE_INVALID` further downstream — no caller looks at
88    /// the raw integer except as a `match` discriminant.
89    pub fn to_errno(&self) -> i32 {
90        match self {
91            MountError::NotFound(_) | MountError::UnknownThread(_) => libc::ENOENT,
92            MountError::Stale(_) => stale_errno(),
93            MountError::NotADirectory(_) => libc::ENOTDIR,
94            MountError::ReadOnly => libc::EROFS,
95            MountError::AlreadyExists(_) => libc::EEXIST,
96            MountError::IsADirectory(_) => libc::EISDIR,
97            MountError::NotEmpty(_) => libc::ENOTEMPTY,
98            MountError::InvalidArgument(_) => libc::EINVAL,
99            MountError::FileTooLarge(_) => libc::EFBIG,
100            MountError::SessionInit(_) => libc::EIO,
101            MountError::Store(HeddleError::NotFound(_))
102            | MountError::Store(HeddleError::StateNotFound(_))
103            | MountError::Store(HeddleError::MissingObject { .. }) => libc::ENOENT,
104            MountError::Store(HeddleError::Io(io)) => io.raw_os_error().unwrap_or(libc::EIO),
105            MountError::Store(_) => libc::EIO,
106        }
107    }
108}
109
110#[cfg(unix)]
111#[inline]
112fn stale_errno() -> i32 {
113    libc::ESTALE
114}
115
116/// POSIX `ESTALE = 116` on Linux. Reuse the value verbatim on
117/// Windows where the libc crate doesn't expose the constant. The
118/// ProjFS errno→Win32 table in `projfs.rs` maps this back to
119/// `ERROR_FILE_INVALID (1632)`.
120#[cfg(windows)]
121#[inline]
122fn stale_errno() -> i32 {
123    116
124}
125
126/// Best-effort stringification of a `catch_unwind` panic payload.
127/// Recovers the common `&'static str` / `String` panic messages and
128/// falls back to a placeholder for anything else. Shared by the
129/// per-OS shell guard wrappers (FUSE / FSKit / ProjFS), which each
130/// catch panics across an `extern "C"` frame and log the message.
131/// Gated to the union of the shell backends so a no-shell build (which
132/// compiles none of the callers) doesn't trip `dead_code`.
133#[cfg(any(
134    all(target_os = "linux", feature = "fuse"),
135    all(target_os = "macos", feature = "fskit"),
136    all(target_os = "windows", feature = "projfs"),
137))]
138pub(crate) fn panic_payload_str(payload: &Box<dyn std::any::Any + Send>) -> String {
139    if let Some(s) = payload.downcast_ref::<&'static str>() {
140        (*s).to_string()
141    } else if let Some(s) = payload.downcast_ref::<String>() {
142        s.clone()
143    } else {
144        "<non-string panic payload>".to_string()
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use std::io;
151
152    use objects::error::HeddleError;
153
154    use super::*;
155
156    #[test]
157    fn to_errno_maps_every_mount_error_variant() {
158        assert_eq!(MountError::NotFound("x".into()).to_errno(), libc::ENOENT);
159        assert_eq!(
160            MountError::UnknownThread("t".into()).to_errno(),
161            libc::ENOENT
162        );
163        assert_eq!(MountError::Stale("s".into()).to_errno(), stale_errno());
164        assert_eq!(
165            MountError::NotADirectory("d".into()).to_errno(),
166            libc::ENOTDIR
167        );
168        assert_eq!(MountError::ReadOnly.to_errno(), libc::EROFS);
169        assert_eq!(
170            MountError::AlreadyExists("e".into()).to_errno(),
171            libc::EEXIST
172        );
173        assert_eq!(
174            MountError::IsADirectory("d".into()).to_errno(),
175            libc::EISDIR
176        );
177        assert_eq!(MountError::NotEmpty("d".into()).to_errno(), libc::ENOTEMPTY);
178        assert_eq!(
179            MountError::InvalidArgument("a".into()).to_errno(),
180            libc::EINVAL
181        );
182        assert_eq!(MountError::FileTooLarge("f".into()).to_errno(), libc::EFBIG);
183        assert_eq!(MountError::SessionInit("s".into()).to_errno(), libc::EIO);
184        assert_eq!(
185            MountError::Store(HeddleError::NotFound("o".into())).to_errno(),
186            libc::ENOENT
187        );
188        assert_eq!(
189            MountError::Store(HeddleError::StateNotFound(
190                objects::object::StateId::from_bytes([0xab; 32])
191            ))
192            .to_errno(),
193            libc::ENOENT
194        );
195        assert_eq!(
196            MountError::Store(HeddleError::MissingObject {
197                object_type: "blob".into(),
198                id: "deadbeef".into(),
199            })
200            .to_errno(),
201            libc::ENOENT
202        );
203        let io_err = io::Error::from_raw_os_error(libc::EACCES);
204        assert_eq!(
205            MountError::Store(HeddleError::Io(io_err)).to_errno(),
206            libc::EACCES
207        );
208        // Non-NotFound store errors fall through to EIO.
209        assert_eq!(
210            MountError::Store(HeddleError::InvalidObject("c".into())).to_errno(),
211            libc::EIO
212        );
213    }
214}