1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
use std::io;
use std::path::{Path, PathBuf};
pub type Result<T> = std::result::Result<T, FsError>;
/// A step that runs after an atomic write has already published the new content. See
/// [`FsError::ReplacedButPostStepFailed`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PostPublishStep {
/// Removing the temp name that is left behind when publication is a hard link followed by an
/// unlink (the fallback where the kernel has no exclusive rename). The content is published and
/// complete; a leftover temp name remains. Generic sweeps retain ambiguous names.
TempCleanup,
/// Removing the old tree a directory publication displaced ([`crate::publish_dir::publish_dir`]). The new
/// directory is in place and complete; the displaced one remains under a sibling name that
/// the error's source describes, and the caller may remove it.
OldTreeCleanup,
}
impl std::fmt::Display for PostPublishStep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::TempCleanup => "removing the temp file",
Self::OldTreeCleanup => "removing the replaced directory",
})
}
}
/// Every failure this crate reports. The enum is `#[non_exhaustive]`: match with a wildcard arm,
/// because variants are added as the crate learns to report more precisely (0.3.4 added
/// [`FsError::ReplacedButDirSyncFailed`], [`FsError::DirSyncFailed`] and
/// [`FsError::ReplacedButPostStepFailed`]).
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum FsError {
#[error("I/O failure on {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("{0} is or traverses a symlink / reparse point, which is refused")]
Symlink(PathBuf),
#[error("{0} is not a directory")]
NotADirectory(PathBuf),
#[error("{0} is not a regular file")]
NotAFile(PathBuf),
#[error("{0} is not owned by the current user")]
NotOwned(PathBuf),
/// A file that was required to have exactly one name has more than one hard link, so another
/// path (possibly outside the approved folder) reaches the same bytes. See
/// [`crate::handle::ReadOptions::require_single_link`].
#[error("{0} has more than one hard link")]
HardLinked(PathBuf),
#[error("{0} is writable by group or others")]
TooPermissive(PathBuf),
/// A private (owner-only) file or directory was asked for where its owner-only guarantee cannot
/// be established because of an extended ACL (macOS): the parent directory has an inheritable
/// `allow` entry that the new entry would receive, the new entry carries an `allow` entry
/// anyway (it is removed, still empty), or an existing entry handed to `restrict_owner_only`
/// carries one (it is left as it is, never repaired). An ACL made only of `deny` entries never
/// causes this error. See [`crate::private`] and the `acl` notes in the crate README.
#[error(
"{0} cannot be made owner-only: an extended ACL `allow` entry would grant access to others"
)]
UnsafeAcl(PathBuf),
#[error("directory identity changed while it was being pinned: {0}")]
IdentityChanged(PathBuf),
#[error("invalid entry name {0:?}")]
BadName(String),
#[error("{0} already exists")]
AlreadyExists(PathBuf),
#[error("{path} is {size} bytes, over the {limit} byte limit")]
TooLarge {
path: PathBuf,
size: u64,
limit: u64,
},
#[error("{path} is locked by {owner}")]
Locked { path: PathBuf, owner: String },
#[error("timed out after {waited_ms} ms waiting for lock {path}")]
LockTimeout { path: PathBuf, waited_ms: u64 },
#[error("entry cap of {0} exceeded")]
TooManyEntries(usize),
#[error("corrupt record {path}: {reason}")]
Corrupt { path: PathBuf, reason: String },
/// A file this operation had verified (its identity, size and modification time, or the
/// content hash the caller expected) was found different from what was recorded, so someone
/// else wrote or replaced it. The operation stopped without overwriting it. Reported by
/// [`crate::transaction`] (feature `transaction`) when a destination, a candidate or the
/// retained material of an earlier step no longer matches; nothing is deleted or replaced on
/// the strength of material that does not match. Added in 0.4.6.
#[error("{path} changed outside this operation: {reason}")]
Conflict { path: PathBuf, reason: String },
/// A copy's digest did not match: the source did not hash to the expected value, or the
/// written copy did not hash to what was read (see [`crate::copy_verified`]). Nothing was
/// published.
#[error("digest mismatch for {path}: expected {expected}, got {actual}")]
DigestMismatch {
path: PathBuf,
expected: String,
actual: String,
},
#[error("unknown stable id {0}")]
UnknownId(String),
#[error("platform primitive unavailable: {0}")]
Unavailable(&'static str),
/// The new content is in place at `path`, but syncing its directory failed, so the rename
/// may not survive a power loss. Retrying the write is safe.
#[error("{} was replaced, but syncing its directory failed: {source}", path.display())]
ReplacedButDirSyncFailed {
path: PathBuf,
#[source]
source: io::Error,
},
/// A `StateDir` removal or directory creation took effect, but syncing the containing
/// directory failed, so it may not survive a power loss. For a directory a call created, the
/// directory stays, and a repeated call finds it as an existing directory and does not sync
/// its link again (the residual in the `atomic` module docs, section 3): a caller that needs
/// that link durable must not take a successful retry as proof of it.
#[error("{} was changed, but syncing its directory failed: {source}", path.display())]
DirSyncFailed {
path: PathBuf,
#[source]
source: io::Error,
},
/// The new content is published at `path` and any required directory sync has run, but a step
/// after publication failed (see [`PostPublishStep`]). The destination holds the new content,
/// so [`FsError::target_replaced`] is true and retrying the write is safe. When several
/// post-publication steps fail, the most serious is reported: the directory sync
/// ([`FsError::ReplacedButDirSyncFailed`]) first, then temp cleanup.
#[error("{} was replaced, but {step} failed: {source}", path.display())]
ReplacedButPostStepFailed {
path: PathBuf,
step: PostPublishStep,
#[source]
source: io::Error,
},
}
impl FsError {
pub fn io(path: impl AsRef<Path>, source: io::Error) -> Self {
Self::Io {
path: path.as_ref().to_path_buf(),
source,
}
}
/// The underlying `io::ErrorKind` when this is an I/O (or already-exists) failure.
pub fn kind(&self) -> Option<io::ErrorKind> {
match self {
Self::Io { source, .. }
| Self::ReplacedButDirSyncFailed { source, .. }
| Self::ReplacedButPostStepFailed { source, .. }
| Self::DirSyncFailed { source, .. } => Some(source.kind()),
Self::AlreadyExists(_) => Some(io::ErrorKind::AlreadyExists),
_ => None,
}
}
/// True when the destination already holds the new content, even though the call failed.
pub fn target_replaced(&self) -> bool {
matches!(
self,
Self::ReplacedButDirSyncFailed { .. } | Self::ReplacedButPostStepFailed { .. }
)
}
pub fn is_not_found(&self) -> bool {
self.kind() == Some(io::ErrorKind::NotFound)
}
/// The `io::ErrorKind` a caller of `std::io` should see for this failure (see the
/// `From<FsError> for io::Error` impl). Variants that carry an `io::Error` keep its kind.
fn io_kind(&self) -> io::ErrorKind {
use io::ErrorKind as K;
match self {
Self::Io { source, .. }
| Self::ReplacedButDirSyncFailed { source, .. }
| Self::ReplacedButPostStepFailed { source, .. }
| Self::DirSyncFailed { source, .. } => source.kind(),
Self::Symlink(_) | Self::NotAFile(_) | Self::BadName(_) => K::InvalidInput,
Self::NotADirectory(_) => K::NotADirectory,
Self::NotOwned(_)
| Self::TooPermissive(_)
| Self::UnsafeAcl(_)
| Self::HardLinked(_) => K::PermissionDenied,
Self::IdentityChanged(_) => K::Other,
Self::AlreadyExists(_) => K::AlreadyExists,
Self::TooLarge { .. } => K::FileTooLarge,
Self::Locked { .. } => K::WouldBlock,
Self::LockTimeout { .. } => K::TimedOut,
Self::TooManyEntries(_) => K::Other,
Self::Corrupt { .. } | Self::DigestMismatch { .. } => K::InvalidData,
Self::Conflict { .. } => K::Other,
Self::UnknownId(_) => K::NotFound,
Self::Unavailable(_) => K::Unsupported,
}
}
}
/// Lets a caller of `std::io` keep the kind (`AlreadyExists`, `NotFound`, `PermissionDenied`,
/// and so on) without matching on message text. The original `FsError` is kept as the
/// `io::Error`'s inner error, so `get_ref()` and `into_inner()` recover it and its `source()` chain
/// still reaches the OS error.
impl From<FsError> for io::Error {
fn from(err: FsError) -> Self {
let kind = err.io_kind();
io::Error::new(kind, err)
}
}