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
//! Errors returned by the ext4rs driver.
use std::io;
/// `#[non_exhaustive]` (#120): a new way to fail is a new variant, and without
/// this every variant added was a break for any caller matching exhaustively.
/// A `match` outside this crate needs a `_` arm.
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
/// Underlying device I/O failure.
Io(io::Error),
/// Magic number mismatch — not an ext4 filesystem.
BadMagic { found: u16, expected: u16 },
/// On-disk structure failed checksum validation.
BadChecksum { what: &'static str },
/// Filesystem uses an INCOMPAT feature we don't implement.
UnsupportedIncompat(u32),
/// Filesystem uses a RO_COMPAT feature we don't implement (read-only mount required anyway).
UnsupportedRoCompat(u32),
/// Path component or inode not found.
NotFound,
/// Path resolution hit a non-directory mid-walk.
NotADirectory,
/// Operation refused because the target is a directory and the syscall
/// only operates on regular files (POSIX EISDIR — used by `unlink(2)`,
/// `truncate(2)`, etc).
IsADirectory,
/// Target already exists (POSIX EEXIST — used by `create(2)`, `mkdir(2)`,
/// `rename(2)` when no-replace is requested).
AlreadyExists,
/// Directory must be empty (POSIX ENOTEMPTY — used by `rmdir(2)`,
/// `rename(2)` replacing a non-empty dir).
DirectoryNotEmpty,
/// Write attempted on a device opened read-only (POSIX EROFS).
ReadOnly,
/// Path component longer than EXT4_NAME_LEN (255 bytes) — POSIX
/// ENAMETOOLONG.
NameTooLong,
/// Container (xattr in-inode region, xattr block, etc.) has no free
/// space for the requested entry — POSIX ENOSPC.
NoSpaceLeftOnDevice,
/// Caller passed a malformed argument or attempted a semantically
/// invalid mutation (POSIX EINVAL — e.g. truncate-grow, moving a dir
/// into its own subtree, operating on a legacy non-EXTENTS inode).
InvalidArgument(&'static str),
/// Inode number is invalid (0, > total inodes, etc.).
InvalidInode(u32),
/// Block number is invalid (> total blocks).
InvalidBlock(u64),
/// Read/write past end of file.
OutOfBounds,
/// Extent tree structure is corrupt.
CorruptExtentTree(&'static str),
/// Directory entry corrupt (rec_len out of range, name too long, etc.).
CorruptDirEntry(&'static str),
/// Generic spec-violation error.
Corrupt(&'static str),
/// The inode's link count has no room for another link (POSIX
/// EMLINK): a file at `MAX_LINKS` (65000) links, or a directory
/// with that many on a volume without `DIR_NLINK`.
TooManyLinks,
/// A structure this driver can read but cannot correctly modify.
/// Distinct from [`Error::ReadOnly`], which is about the mount, and
/// from the `Unsupported*` feature errors, which are about the volume:
/// this is one operation on one object being refused because doing it
/// would leave the filesystem worse than not doing it.
Unsupported(&'static str),
/// A write refused because the volume's journal holds committed
/// transactions this mount has not yet replayed onto the device: a
/// lazy mount before [`crate::fs::Filesystem::replay_journal_if_dirty`].
/// Writing first would commit over the unreplayed log and lose it.
/// Maps to EROFS: the mount is read-only until the replay runs.
JournalNotReplayed,
/// An inode handle no longer names the file it was taken from (POSIX
/// ESTALE): the inode number is out of range or reserved, the inode has
/// been freed, or it has been freed and reused under a new generation.
Stale,
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::Io(e)
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Io(e) => write!(f, "I/O error: {e}"),
Error::BadMagic { found, expected } => {
write!(f, "bad magic: 0x{found:04x} (expected 0x{expected:04x})")
}
Error::BadChecksum { what } => write!(f, "{what} checksum mismatch"),
Error::UnsupportedIncompat(bits) => {
write!(f, "unsupported INCOMPAT features: 0x{bits:08x}")
}
Error::UnsupportedRoCompat(bits) => {
write!(f, "unsupported RO_COMPAT features: 0x{bits:08x}")
}
Error::NotFound => write!(f, "not found"),
Error::NotADirectory => write!(f, "not a directory"),
Error::IsADirectory => write!(f, "is a directory"),
Error::AlreadyExists => write!(f, "already exists"),
Error::DirectoryNotEmpty => write!(f, "directory not empty"),
Error::ReadOnly => write!(f, "read-only filesystem"),
Error::NameTooLong => write!(f, "name too long"),
Error::NoSpaceLeftOnDevice => write!(f, "no space left on device"),
Error::InvalidArgument(msg) => write!(f, "invalid argument: {msg}"),
Error::InvalidInode(n) => write!(f, "invalid inode number {n}"),
Error::InvalidBlock(n) => write!(f, "invalid block number {n}"),
Error::OutOfBounds => write!(f, "out of bounds"),
Error::CorruptExtentTree(msg) => write!(f, "corrupt extent tree: {msg}"),
Error::CorruptDirEntry(msg) => write!(f, "corrupt directory entry: {msg}"),
Error::Corrupt(msg) => write!(f, "corrupt: {msg}"),
Error::Unsupported(msg) => write!(f, "unsupported: {msg}"),
Error::JournalNotReplayed => {
write!(f, "read-only until the dirty journal is replayed")
}
Error::Stale => write!(f, "stale inode handle"),
Error::TooManyLinks => write!(f, "too many links"),
}
}
}
impl std::error::Error for Error {}
pub type Result<T> = std::result::Result<T, Error>;
impl Error {
/// Map an error to a POSIX errno suitable for the C ABI / FSKit.
///
/// Values are macOS POSIX errno numbers (which match Linux for every
/// code used here — the divergence only starts at ENOTSUP).
pub fn to_errno(&self) -> i32 {
match self {
Error::Io(e) => e.raw_os_error().unwrap_or(EIO),
Error::NotFound => ENOENT,
Error::NotADirectory => ENOTDIR,
Error::IsADirectory => EISDIR,
Error::AlreadyExists => EEXIST,
Error::DirectoryNotEmpty => ENOTEMPTY,
Error::ReadOnly | Error::JournalNotReplayed => EROFS,
Error::NameTooLong => ENAMETOOLONG,
Error::NoSpaceLeftOnDevice => ENOSPC,
Error::TooManyLinks => EMLINK,
Error::InvalidArgument(_) => EINVAL,
Error::InvalidInode(_) | Error::InvalidBlock(_) | Error::OutOfBounds => EINVAL,
Error::BadMagic { .. }
| Error::BadChecksum { .. }
| Error::CorruptExtentTree(_)
| Error::CorruptDirEntry(_)
| Error::Corrupt(_) => EIO,
Error::UnsupportedIncompat(_)
| Error::UnsupportedRoCompat(_)
| Error::Unsupported(_) => ENOTSUP,
Error::Stale => ESTALE,
}
}
}
/// POSIX errno values (macOS). Also match Linux for all codes referenced here
/// except ENOTSUP (Linux uses 95, macOS uses 45). We target FSKit on macOS.
pub mod errno {
pub const ENOENT: i32 = 2;
pub const EIO: i32 = 5;
pub const EEXIST: i32 = 17;
pub const ENOTDIR: i32 = 20;
pub const EISDIR: i32 = 21;
pub const EINVAL: i32 = 22;
pub const EROFS: i32 = 30;
pub const EMLINK: i32 = 31;
pub const ERANGE: i32 = 34;
pub const ENOSPC: i32 = 28;
pub const ENAMETOOLONG: i32 = 63; // macOS POSIX value
pub const ENOTSUP: i32 = 45;
pub const ENOTEMPTY: i32 = 66; // macOS POSIX value
pub const ESTALE: i32 = 70; // macOS POSIX value (Linux: 116)
pub const ENOSYS: i32 = 78; // macOS POSIX value (Linux: 38). Surfaced
// by capi when a feature isn't implemented yet.
}
use errno::*;