use std::fmt;
use std::io;
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub struct FsError {
pub code: String,
pub description: String,
pub syscall: String,
pub path: Option<String>,
}
impl fmt::Display for FsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}, {}", self.code, self.description, self.syscall)?;
if let Some(path) = &self.path {
write!(f, " '{path}'")?;
}
Ok(())
}
}
impl FsError {
#[must_use]
pub fn new(code: &str, syscall: &str, path: Option<&str>) -> Self {
Self {
code: code.to_owned(),
description: describe(code).to_owned(),
syscall: syscall.to_owned(),
path: path.map(str::to_owned),
}
}
#[must_use]
pub fn from_io(error: &io::Error, syscall: &str, path: Option<&str>) -> Self {
Self::new(errno_name(error), syscall, path)
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("spawn {program} {code}")]
pub struct SpawnError {
pub program: String,
pub code: String,
}
impl SpawnError {
#[must_use]
pub fn from_io(error: &io::Error, program: &str) -> Self {
Self {
program: program.to_owned(),
code: errno_name(error).to_owned(),
}
}
}
fn errno_name(error: &io::Error) -> &'static str {
#[cfg(unix)]
if let Some(name) = error.raw_os_error().and_then(unix_errno_name) {
return name;
}
match error.kind() {
io::ErrorKind::NotFound => "ENOENT",
io::ErrorKind::PermissionDenied => "EACCES",
io::ErrorKind::AlreadyExists => "EEXIST",
io::ErrorKind::IsADirectory => "EISDIR",
io::ErrorKind::NotADirectory => "ENOTDIR",
io::ErrorKind::DirectoryNotEmpty => "ENOTEMPTY",
io::ErrorKind::ReadOnlyFilesystem => "EROFS",
io::ErrorKind::StorageFull => "ENOSPC",
io::ErrorKind::InvalidFilename => "ENAMETOOLONG",
io::ErrorKind::InvalidInput => "EINVAL",
io::ErrorKind::ResourceBusy => "EBUSY",
_ => "UNKNOWN",
}
}
#[cfg(unix)]
fn unix_errno_name(raw: i32) -> Option<&'static str> {
use nix::errno::Errno;
let name = match Errno::from_raw(raw) {
Errno::ENOENT => "ENOENT",
Errno::EACCES => "EACCES",
Errno::EEXIST => "EEXIST",
Errno::EISDIR => "EISDIR",
Errno::ENOTDIR => "ENOTDIR",
Errno::ENOTEMPTY => "ENOTEMPTY",
Errno::EPERM => "EPERM",
Errno::ENOSPC => "ENOSPC",
Errno::EROFS => "EROFS",
Errno::ELOOP => "ELOOP",
Errno::ENAMETOOLONG => "ENAMETOOLONG",
Errno::EBUSY => "EBUSY",
Errno::EMFILE => "EMFILE",
Errno::EINVAL => "EINVAL",
Errno::EIO => "EIO",
Errno::EXDEV => "EXDEV",
Errno::ETXTBSY => "ETXTBSY",
Errno::EAGAIN => "EAGAIN",
Errno::EBADF => "EBADF",
_ => return None,
};
Some(name)
}
fn describe(code: &str) -> &'static str {
match code {
"ENOENT" => "no such file or directory",
"EACCES" => "permission denied",
"EEXIST" => "file already exists",
"EISDIR" => "illegal operation on a directory",
"ENOTDIR" => "not a directory",
"ENOTEMPTY" => "directory not empty",
"EPERM" => "operation not permitted",
"ENOSPC" => "no space left on device",
"EROFS" => "read-only file system",
"ELOOP" => "too many symbolic links encountered",
"ENAMETOOLONG" => "name too long",
"EBUSY" => "resource busy or locked",
"EMFILE" => "too many open files",
"EINVAL" => "invalid argument",
"EIO" => "i/o error",
"EXDEV" => "cross-device link not permitted",
"ETXTBSY" => "text file is busy",
"EAGAIN" => "resource temporarily unavailable",
"EBADF" => "bad file descriptor",
_ => "unknown error",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn renders_like_node() {
let error = FsError::new("ENOENT", "open", Some("/x"));
assert_eq!(
error.to_string(),
"ENOENT: no such file or directory, open '/x'"
);
let error = FsError::new("EISDIR", "read", None);
assert_eq!(
error.to_string(),
"EISDIR: illegal operation on a directory, read"
);
}
#[test]
fn maps_io_kinds() {
let error = io::Error::from(io::ErrorKind::NotFound);
assert_eq!(FsError::from_io(&error, "stat", None).code, "ENOENT");
}
}