use std::fmt;
use std::path::PathBuf;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
#[non_exhaustive]
#[must_use = "errors should be inspected, propagated, or logged"]
pub enum Error {
Io(std::io::Error),
InvalidPath {
path: PathBuf,
reason: String,
},
HardwareProbeFailed {
detail: String,
},
UnsupportedPlatform {
detail: String,
},
UnsupportedMethod {
method: &'static str,
},
AlignmentRequired {
detail: &'static str,
},
AtomicReplaceFailed {
step: &'static str,
source: std::io::Error,
},
PartialDirectoryOp {
failed_step: String,
completed_steps: Vec<String>,
},
ShutdownInProgress,
QueueFull,
}
impl Error {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Error::Io(_) => "FS-00001",
Error::InvalidPath { .. } => "FS-00002",
Error::HardwareProbeFailed { .. } => "FS-00003",
Error::UnsupportedPlatform { .. } => "FS-00004",
Error::UnsupportedMethod { .. } => "FS-00005",
Error::AlignmentRequired { .. } => "FS-00006",
Error::AtomicReplaceFailed { .. } => "FS-00007",
Error::PartialDirectoryOp { .. } => "FS-00008",
Error::ShutdownInProgress => "FS-00009",
Error::QueueFull => "FS-00010",
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Io(e) => write!(f, "[{}] io error: {}", self.code(), e),
Error::InvalidPath { path, reason } => write!(
f,
"[{}] invalid path {:?}: {}",
self.code(),
path.display(),
reason
),
Error::HardwareProbeFailed { detail } => {
write!(f, "[{}] hardware probe failed: {}", self.code(), detail)
}
Error::UnsupportedPlatform { detail } => {
write!(f, "[{}] unsupported platform: {}", self.code(), detail)
}
Error::UnsupportedMethod { method } => {
write!(
f,
"[{}] method '{}' is not implemented in this release",
self.code(),
method
)
}
Error::AlignmentRequired { detail } => {
write!(
f,
"[{}] alignment requirement failed: {}",
self.code(),
detail
)
}
Error::AtomicReplaceFailed { step, source } => {
write!(
f,
"[{}] atomic write-replace failed at step '{}': {}",
self.code(),
step,
source
)
}
Error::PartialDirectoryOp {
failed_step,
completed_steps,
} => {
write!(
f,
"[{}] directory op failed at '{}' after {} completed step(s)",
self.code(),
failed_step,
completed_steps.len()
)
}
Error::ShutdownInProgress => {
write!(
f,
"[{}] handle is shutting down; batch submission rejected",
self.code()
)
}
Error::QueueFull => {
write!(
f,
"[{}] group-lane queue is full (reserved variant; never emitted in 0.4.0)",
self.code()
)
}
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io(e) => Some(e),
Error::AtomicReplaceFailed { source, .. } => Some(source),
Error::InvalidPath { .. }
| Error::HardwareProbeFailed { .. }
| Error::UnsupportedPlatform { .. }
| Error::UnsupportedMethod { .. }
| Error::AlignmentRequired { .. }
| Error::PartialDirectoryOp { .. }
| Error::ShutdownInProgress
| Error::QueueFull => None,
}
}
}
impl From<std::io::Error> for Error {
fn from(value: std::io::Error) -> Self {
Error::Io(value)
}
}
#[derive(Debug)]
#[non_exhaustive]
#[must_use = "errors should be inspected, propagated, or logged"]
pub struct BatchError {
pub failed_at: usize,
pub completed: usize,
pub source: Box<Error>,
}
impl BatchError {
pub fn inner(&self) -> &Error {
&self.source
}
pub fn into_inner(self) -> Box<Error> {
self.source
}
}
impl fmt::Display for BatchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"batch failed at op {} after {} successful op(s): {}",
self.failed_at, self.completed, self.source
)
}
}
impl std::error::Error for BatchError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&*self.source)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io;
#[test]
fn test_error_code_io_returns_fs00001() {
let err = Error::Io(io::Error::from(io::ErrorKind::NotFound));
assert_eq!(err.code(), "FS-00001");
}
#[test]
fn test_error_code_invalid_path_returns_fs00002() {
let err = Error::InvalidPath {
path: PathBuf::from("bad"),
reason: "empty segment".into(),
};
assert_eq!(err.code(), "FS-00002");
}
#[test]
fn test_error_code_hardware_probe_returns_fs00003() {
let err = Error::HardwareProbeFailed {
detail: "nvme ioctl unavailable".into(),
};
assert_eq!(err.code(), "FS-00003");
}
#[test]
fn test_error_code_unsupported_platform_returns_fs00004() {
let err = Error::UnsupportedPlatform {
detail: "io_uring requires Linux 5.1+".into(),
};
assert_eq!(err.code(), "FS-00004");
}
#[test]
fn test_error_display_unsupported_platform_includes_detail() {
let err = Error::UnsupportedPlatform {
detail: "io_uring not available".into(),
};
let s = err.to_string();
assert!(s.starts_with("[FS-00004]"));
assert!(s.contains("io_uring not available"));
}
#[test]
fn test_error_display_io_includes_code_and_kind() {
let err = Error::Io(io::Error::from(io::ErrorKind::NotFound));
let s = err.to_string();
assert!(s.starts_with("[FS-00001]"));
assert!(s.contains("io error"));
}
#[test]
fn test_error_display_invalid_path_does_not_panic_on_unicode() {
let err = Error::InvalidPath {
path: PathBuf::from("名前/test"),
reason: "rejected".into(),
};
let s = err.to_string();
assert!(s.contains("FS-00002"));
}
#[test]
fn test_error_source_io_returns_inner() {
let inner = io::Error::from(io::ErrorKind::PermissionDenied);
let err = Error::Io(inner);
assert!(std::error::Error::source(&err).is_some());
}
#[test]
fn test_error_source_invalid_path_returns_none() {
let err = Error::InvalidPath {
path: PathBuf::from("x"),
reason: "y".into(),
};
assert!(std::error::Error::source(&err).is_none());
}
#[test]
fn test_error_from_io_error_converts() {
let io_err = io::Error::from(io::ErrorKind::Other);
let err: Error = io_err.into();
assert_eq!(err.code(), "FS-00001");
}
#[test]
fn test_result_alias_compiles_for_ok_and_err_paths() {
fn returns_ok() -> Result<u8> {
Ok(1)
}
fn returns_err() -> Result<u8> {
Err(Error::HardwareProbeFailed {
detail: "test".into(),
})
}
assert_eq!(returns_ok().ok(), Some(1));
assert!(returns_err().is_err());
}
#[test]
fn test_error_code_unsupported_method_returns_fs00005() {
let err = Error::UnsupportedMethod { method: "Mmap" };
assert_eq!(err.code(), "FS-00005");
}
#[test]
fn test_error_display_unsupported_method_includes_name() {
let err = Error::UnsupportedMethod { method: "Journal" };
let s = err.to_string();
assert!(s.starts_with("[FS-00005]"));
assert!(s.contains("Journal"));
}
#[test]
fn test_error_code_alignment_required_returns_fs00006() {
let err = Error::AlignmentRequired {
detail: "size not a multiple of sector size",
};
assert_eq!(err.code(), "FS-00006");
}
#[test]
fn test_error_display_alignment_required_includes_detail() {
let err = Error::AlignmentRequired {
detail: "buffer not aligned to 4096",
};
let s = err.to_string();
assert!(s.starts_with("[FS-00006]"));
assert!(s.contains("4096"));
}
#[test]
fn test_error_code_atomic_replace_failed_returns_fs00007() {
let err = Error::AtomicReplaceFailed {
step: "rename",
source: io::Error::from(io::ErrorKind::PermissionDenied),
};
assert_eq!(err.code(), "FS-00007");
}
#[test]
fn test_error_display_atomic_replace_includes_step() {
let err = Error::AtomicReplaceFailed {
step: "flush",
source: io::Error::from(io::ErrorKind::Other),
};
let s = err.to_string();
assert!(s.starts_with("[FS-00007]"));
assert!(s.contains("flush"));
}
#[test]
fn test_error_source_atomic_replace_returns_inner() {
let err = Error::AtomicReplaceFailed {
step: "write",
source: io::Error::from(io::ErrorKind::NotFound),
};
assert!(std::error::Error::source(&err).is_some());
}
#[test]
fn test_error_code_partial_dir_op_returns_fs00008() {
let err = Error::PartialDirectoryOp {
failed_step: "create /a/b".into(),
completed_steps: vec!["create /a".into()],
};
assert_eq!(err.code(), "FS-00008");
}
#[test]
fn test_error_display_partial_dir_op_includes_step() {
let err = Error::PartialDirectoryOp {
failed_step: "create /a/b/c".into(),
completed_steps: vec!["create /a".into(), "create /a/b".into()],
};
let s = err.to_string();
assert!(s.starts_with("[FS-00008]"));
assert!(s.contains("/a/b/c"));
}
#[test]
fn test_error_source_partial_dir_op_returns_none() {
let err = Error::PartialDirectoryOp {
failed_step: "create /x".into(),
completed_steps: vec![],
};
assert!(std::error::Error::source(&err).is_none());
}
#[test]
fn test_error_code_shutdown_in_progress_returns_fs00009() {
let err = Error::ShutdownInProgress;
assert_eq!(err.code(), "FS-00009");
}
#[test]
fn test_error_display_shutdown_in_progress_includes_code() {
let err = Error::ShutdownInProgress;
let s = err.to_string();
assert!(s.starts_with("[FS-00009]"));
assert!(s.contains("shutting down"));
}
#[test]
fn test_error_source_shutdown_in_progress_returns_none() {
let err = Error::ShutdownInProgress;
assert!(std::error::Error::source(&err).is_none());
}
#[test]
fn test_error_code_queue_full_returns_fs00010() {
let err = Error::QueueFull;
assert_eq!(err.code(), "FS-00010");
}
#[test]
fn test_error_display_queue_full_marked_reserved() {
let err = Error::QueueFull;
let s = err.to_string();
assert!(s.starts_with("[FS-00010]"));
assert!(s.to_ascii_lowercase().contains("reserved"));
}
#[test]
fn test_error_source_queue_full_returns_none() {
let err = Error::QueueFull;
assert!(std::error::Error::source(&err).is_none());
}
#[test]
fn test_batch_error_fields_round_trip() {
let inner = Error::Io(io::Error::from(io::ErrorKind::NotFound));
let be = BatchError {
failed_at: 3,
completed: 3,
source: Box::new(inner),
};
assert_eq!(be.failed_at, 3);
assert_eq!(be.completed, 3);
assert_eq!(be.inner().code(), "FS-00001");
}
#[test]
fn test_batch_error_display_includes_indices_and_inner() {
let inner = Error::HardwareProbeFailed {
detail: "probe stub".into(),
};
let be = BatchError {
failed_at: 7,
completed: 7,
source: Box::new(inner),
};
let s = be.to_string();
assert!(s.contains("op 7"));
assert!(s.contains("7 successful"));
assert!(s.contains("FS-00003"));
}
#[test]
fn test_batch_error_implements_std_error_with_inner_source() {
let inner = Error::Io(io::Error::from(io::ErrorKind::PermissionDenied));
let be = BatchError {
failed_at: 0,
completed: 0,
source: Box::new(inner),
};
let dyn_err: &dyn std::error::Error = &be;
assert!(dyn_err.source().is_some());
}
#[test]
fn test_batch_error_into_inner_returns_boxed_error() {
let inner = Error::ShutdownInProgress;
let be = BatchError {
failed_at: 0,
completed: 0,
source: Box::new(inner),
};
let unboxed: Box<Error> = be.into_inner();
assert_eq!(unboxed.code(), "FS-00009");
}
}