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 /// The platform shell failed to construct its mount session
63 /// (e.g. the Swift FSKit shim returned a null session handle).
64 /// Maps to `EIO`: the mount never came up, nothing to retry
65 /// at the filesystem layer.
66 #[error("mount session initialization failed: {0}")]
67 SessionInit(String),
68
69 /// Errors bubbling up from the underlying object store / repo.
70 #[error(transparent)]
71 Store(#[from] HeddleError),
72}
73
74impl MountError {
75 /// Translate this error into a libc errno suitable for handing
76 /// back to FUSE. Only the platform shell uses this — keeping it
77 /// here means platform code stays one-liners.
78 ///
79 /// `ESTALE` is POSIX-only; on Windows `libc` doesn't define it,
80 /// so the Windows build uses the POSIX value (`116`) verbatim.
81 /// The ProjFS shell translates this back into a Win32
82 /// `ERROR_FILE_INVALID` further downstream — no caller looks at
83 /// the raw integer except as a `match` discriminant.
84 pub fn to_errno(&self) -> i32 {
85 match self {
86 MountError::NotFound(_) | MountError::UnknownThread(_) => libc::ENOENT,
87 MountError::Stale(_) => stale_errno(),
88 MountError::NotADirectory(_) => libc::ENOTDIR,
89 MountError::ReadOnly => libc::EROFS,
90 MountError::AlreadyExists(_) => libc::EEXIST,
91 MountError::IsADirectory(_) => libc::EISDIR,
92 MountError::NotEmpty(_) => libc::ENOTEMPTY,
93 MountError::InvalidArgument(_) => libc::EINVAL,
94 MountError::SessionInit(_) => libc::EIO,
95 MountError::Store(HeddleError::NotFound(_))
96 | MountError::Store(HeddleError::StateNotFound(_))
97 | MountError::Store(HeddleError::MissingObject { .. }) => libc::ENOENT,
98 MountError::Store(HeddleError::Io(io)) => io.raw_os_error().unwrap_or(libc::EIO),
99 MountError::Store(_) => libc::EIO,
100 }
101 }
102}
103
104#[cfg(unix)]
105#[inline]
106fn stale_errno() -> i32 {
107 libc::ESTALE
108}
109
110/// POSIX `ESTALE = 116` on Linux. Reuse the value verbatim on
111/// Windows where the libc crate doesn't expose the constant. The
112/// ProjFS errno→Win32 table in `projfs.rs` maps this back to
113/// `ERROR_FILE_INVALID (1632)`.
114#[cfg(windows)]
115#[inline]
116fn stale_errno() -> i32 {
117 116
118}
119
120/// Best-effort stringification of a `catch_unwind` panic payload.
121/// Recovers the common `&'static str` / `String` panic messages and
122/// falls back to a placeholder for anything else. Shared by the
123/// per-OS shell guard wrappers (FUSE / FSKit / ProjFS), which each
124/// catch panics across an `extern "C"` frame and log the message.
125/// Gated to the union of the shell backends so a no-shell build (which
126/// compiles none of the callers) doesn't trip `dead_code`.
127#[cfg(any(
128 all(target_os = "linux", feature = "fuse"),
129 all(target_os = "macos", feature = "fskit"),
130 all(target_os = "windows", feature = "projfs"),
131))]
132pub(crate) fn panic_payload_str(payload: &Box<dyn std::any::Any + Send>) -> String {
133 if let Some(s) = payload.downcast_ref::<&'static str>() {
134 (*s).to_string()
135 } else if let Some(s) = payload.downcast_ref::<String>() {
136 s.clone()
137 } else {
138 "<non-string panic payload>".to_string()
139 }
140}