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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
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, which [`crate::sweep_stale_temps`] reclaims.
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 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::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)
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Convert and check that the kind is `expected` and the original `FsError` is kept inside.
fn assert_mapped(err: FsError, expected: io::ErrorKind) {
let label = format!("{err:?}");
let io_err: io::Error = err.into();
assert_eq!(io_err.kind(), expected, "{label}");
let inner = io_err
.get_ref()
.and_then(|e| e.downcast_ref::<FsError>())
.unwrap_or_else(|| panic!("{label}: original FsError not kept"));
assert_eq!(inner.io_kind(), expected, "{label}");
}
#[test]
fn io_variant_keeps_the_source_kind() {
let p = PathBuf::from("/x");
assert_mapped(
FsError::io(&p, io::Error::from(io::ErrorKind::PermissionDenied)),
io::ErrorKind::PermissionDenied,
);
assert_mapped(
FsError::io(&p, io::Error::from(io::ErrorKind::NotFound)),
io::ErrorKind::NotFound,
);
}
#[test]
fn already_exists_maps_to_already_exists() {
assert_mapped(
FsError::AlreadyExists(PathBuf::from("/x")),
io::ErrorKind::AlreadyExists,
);
}
#[test]
fn each_other_variant_maps_deliberately() {
let p = PathBuf::from("/x");
let cases: Vec<(FsError, io::ErrorKind)> = vec![
(FsError::Symlink(p.clone()), io::ErrorKind::InvalidInput),
(
FsError::NotADirectory(p.clone()),
io::ErrorKind::NotADirectory,
),
(FsError::NotAFile(p.clone()), io::ErrorKind::InvalidInput),
(
FsError::NotOwned(p.clone()),
io::ErrorKind::PermissionDenied,
),
(
FsError::HardLinked(p.clone()),
io::ErrorKind::PermissionDenied,
),
(
FsError::TooPermissive(p.clone()),
io::ErrorKind::PermissionDenied,
),
(
FsError::UnsafeAcl(p.clone()),
io::ErrorKind::PermissionDenied,
),
(FsError::IdentityChanged(p.clone()), io::ErrorKind::Other),
(FsError::BadName("a/b".into()), io::ErrorKind::InvalidInput),
(
FsError::TooLarge {
path: p.clone(),
size: 9,
limit: 4,
},
io::ErrorKind::FileTooLarge,
),
(
FsError::Locked {
path: p.clone(),
owner: "other".into(),
},
io::ErrorKind::WouldBlock,
),
(
FsError::LockTimeout {
path: p.clone(),
waited_ms: 5,
},
io::ErrorKind::TimedOut,
),
(FsError::TooManyEntries(3), io::ErrorKind::Other),
(
FsError::Corrupt {
path: p.clone(),
reason: "bad".into(),
},
io::ErrorKind::InvalidData,
),
(
FsError::DigestMismatch {
path: p.clone(),
expected: "a".into(),
actual: "b".into(),
},
io::ErrorKind::InvalidData,
),
(FsError::UnknownId("id".into()), io::ErrorKind::NotFound),
(FsError::Unavailable("flock"), io::ErrorKind::Unsupported),
];
for (err, kind) in cases {
assert_mapped(err, kind);
}
}
#[test]
fn post_publish_variants_keep_the_source_kind() {
let p = PathBuf::from("/x");
let source = || io::Error::from(io::ErrorKind::StorageFull);
assert_mapped(
FsError::ReplacedButDirSyncFailed {
path: p.clone(),
source: source(),
},
io::ErrorKind::StorageFull,
);
assert_mapped(
FsError::DirSyncFailed {
path: p.clone(),
source: source(),
},
io::ErrorKind::StorageFull,
);
assert_mapped(
FsError::ReplacedButPostStepFailed {
path: p,
step: PostPublishStep::TempCleanup,
source: source(),
},
io::ErrorKind::StorageFull,
);
}
}