use thiserror::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OperationOutcome {
DefiniteFailure,
SafeToRetry,
Uncertain,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OperationClass {
ReadOnly,
SessionControl,
ReplaySensitive,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RecoveryAction {
Retry,
Reopen,
Remount,
VerifyThenResume,
DoNotRetry,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RequestContext {
pub operation: String,
pub session_id: [u8; 16],
pub slot_id: u32,
pub sequence_id: u32,
}
#[derive(Error, Debug)]
#[error("{outcome:?} NFS operation {operation_class:?}; recovery={recovery:?}")]
pub struct OperationOutcomeError {
pub outcome: OperationOutcome,
pub operation_class: OperationClass,
pub recovery: RecoveryAction,
#[source]
pub source: Box<NfsError>,
context: RequestContext,
}
impl OperationOutcomeError {
pub fn new(
outcome: OperationOutcome,
operation_class: OperationClass,
recovery: RecoveryAction,
context: RequestContext,
source: NfsError,
) -> Self {
Self {
outcome,
operation_class,
recovery,
source: Box::new(source),
context,
}
}
pub fn context(&self) -> &RequestContext {
&self.context
}
}
#[derive(Error, Debug)]
pub enum NfsError {
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("NFS3 error: {0}")]
Nfs3(crate::nfs3::ErrorCode),
#[error("NFS4 error: {0}")]
Nfs4(crate::nfs41::Nfs4ErrorCode),
#[error("Mount error: {0}")]
Mount(crate::nfs3::MountErrorCode),
#[error("RPC error: {0}")]
Rpc(String),
#[error("XDR error: {0}")]
Xdr(String),
#[error("{0}")]
Unsupported(String),
#[error("{0}")]
InvalidInput(String),
#[error("rdattr_error: server returned nfsstat4 {0} for entry attributes")]
RdattrError(u32),
#[error(transparent)]
OperationOutcome(#[from] Box<OperationOutcomeError>),
}
impl NfsError {
pub fn operation_outcome(&self) -> Option<&OperationOutcomeError> {
match self {
Self::OperationOutcome(error) => Some(error),
_ => None,
}
}
pub fn is_exist(&self) -> bool {
matches!(
self,
NfsError::Nfs3(crate::nfs3::ErrorCode::NFS3ERR_EXIST)
| NfsError::Nfs4(crate::nfs41::Nfs4ErrorCode::NFS4ERR_EXIST)
)
}
pub fn is_not_found(&self) -> bool {
matches!(
self,
NfsError::Nfs3(crate::nfs3::ErrorCode::NFS3ERR_NOENT)
| NfsError::Nfs4(crate::nfs41::Nfs4ErrorCode::NFS4ERR_NOENT)
)
}
pub fn kind(&self) -> std::io::ErrorKind {
match self {
NfsError::Io(io) => io.kind(),
NfsError::Nfs3(_) => std::io::ErrorKind::Other,
NfsError::Nfs4(_) => std::io::ErrorKind::Other,
NfsError::Mount(_) => std::io::ErrorKind::Other,
NfsError::Rpc(_) => std::io::ErrorKind::Other,
NfsError::Xdr(_) => std::io::ErrorKind::Other,
NfsError::Unsupported(_) => std::io::ErrorKind::Unsupported,
NfsError::InvalidInput(_) => std::io::ErrorKind::InvalidInput,
NfsError::RdattrError(_) => std::io::ErrorKind::Other,
NfsError::OperationOutcome(_) => std::io::ErrorKind::Other,
}
}
}
pub(crate) fn classify_sent_nfs41_error(
operation_class: OperationClass,
context: RequestContext,
source: NfsError,
) -> NfsError {
let lacks_authoritative_result = matches!(
&source,
NfsError::Io(_) | NfsError::Rpc(_) | NfsError::Xdr(_)
) || matches!(
&source,
NfsError::Nfs4(crate::nfs41::Nfs4ErrorCode::NFS4ERR_RETRY_UNCACHED_REP)
| NfsError::Nfs4(crate::nfs41::Nfs4ErrorCode::NFS4ERR_SEQ_FALSE_RETRY)
);
if !lacks_authoritative_result {
return source;
}
let (outcome, recovery) = match operation_class {
OperationClass::ReadOnly => (OperationOutcome::SafeToRetry, RecoveryAction::Retry),
OperationClass::SessionControl => (OperationOutcome::Uncertain, RecoveryAction::Remount),
OperationClass::ReplaySensitive => (
OperationOutcome::Uncertain,
RecoveryAction::VerifyThenResume,
),
};
NfsError::OperationOutcome(Box::new(OperationOutcomeError::new(
outcome,
operation_class,
recovery,
context,
source,
)))
}
pub type Result<T> = std::result::Result<T, NfsError>;
impl From<NfsError> for std::io::Error {
fn from(e: NfsError) -> Self {
match e {
NfsError::Io(io) => io,
NfsError::Nfs3(code) => std::io::Error::other(code),
NfsError::Nfs4(code) => std::io::Error::other(code),
NfsError::Mount(code) => std::io::Error::other(code),
NfsError::Rpc(msg) => std::io::Error::other(msg),
NfsError::Xdr(msg) => std::io::Error::other(msg),
NfsError::Unsupported(msg) => std::io::Error::new(std::io::ErrorKind::Unsupported, msg),
NfsError::InvalidInput(msg) => {
std::io::Error::new(std::io::ErrorKind::InvalidInput, msg)
}
NfsError::RdattrError(code) => {
std::io::Error::other(format!("rdattr_error: nfsstat4 {}", code))
}
NfsError::OperationOutcome(error) => std::io::Error::other(error),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
#[test]
fn nfs3_error_display() {
let err = NfsError::Nfs3(crate::nfs3::ErrorCode::NFS3ERR_PERM);
assert!(err.to_string().contains("NFS3 error"));
}
#[test]
fn nfs4_error_display() {
let err = NfsError::Nfs4(crate::nfs41::Nfs4ErrorCode::NFS4ERR_PERM);
assert!(err.to_string().contains("NFS4 error"));
assert!(err.to_string().contains("permission denied"));
}
#[test]
fn nfs4_error_kind_is_other() {
let err = NfsError::Nfs4(crate::nfs41::Nfs4ErrorCode::NFS4ERR_STALE);
assert_eq!(err.kind(), std::io::ErrorKind::Other);
}
#[test]
fn mount_error_display() {
let err = NfsError::Mount(crate::nfs3::MountErrorCode::MNT3ERR_PERM);
assert!(err.to_string().contains("Mount error"));
}
#[test]
fn rpc_error_display() {
let err = NfsError::Rpc("bad response".to_string());
assert_eq!(err.to_string(), "RPC error: bad response");
}
#[test]
fn xdr_error_display() {
let err = NfsError::Xdr("truncated".to_string());
assert_eq!(err.to_string(), "XDR error: truncated");
}
#[test]
fn unsupported_display() {
let err = NfsError::Unsupported("NFSv4 required".to_string());
assert_eq!(err.to_string(), "NFSv4 required");
}
#[test]
fn invalid_input_display() {
let err = NfsError::InvalidInput("bad URL".to_string());
assert_eq!(err.to_string(), "bad URL");
}
#[test]
fn io_error_transparent_display() {
let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "connection lost");
let err = NfsError::Io(io_err);
assert_eq!(err.to_string(), "connection lost");
}
#[test]
fn from_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
let nfs_err: NfsError = io_err.into();
assert!(matches!(nfs_err, NfsError::Io(_)));
}
#[test]
fn kind_io_preserves_inner() {
let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
let err = NfsError::Io(io_err);
assert_eq!(err.kind(), std::io::ErrorKind::ConnectionRefused);
}
#[test]
fn kind_nfs3_is_other() {
let err = NfsError::Nfs3(crate::nfs3::ErrorCode::NFS3ERR_NOENT);
assert_eq!(err.kind(), std::io::ErrorKind::Other);
}
#[test]
fn kind_unsupported() {
let err = NfsError::Unsupported("test".to_string());
assert_eq!(err.kind(), std::io::ErrorKind::Unsupported);
}
#[test]
fn kind_invalid_input() {
let err = NfsError::InvalidInput("test".to_string());
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
}
#[test]
fn is_exist_nfs3() {
let err = NfsError::Nfs3(crate::nfs3::ErrorCode::NFS3ERR_EXIST);
assert!(err.is_exist());
}
#[test]
fn is_exist_nfs4() {
let err = NfsError::Nfs4(crate::nfs41::Nfs4ErrorCode::NFS4ERR_EXIST);
assert!(err.is_exist());
}
#[test]
fn is_exist_false_for_other() {
let err = NfsError::Nfs4(crate::nfs41::Nfs4ErrorCode::NFS4ERR_NOENT);
assert!(!err.is_exist());
}
#[test]
fn is_not_found_nfs3() {
let err = NfsError::Nfs3(crate::nfs3::ErrorCode::NFS3ERR_NOENT);
assert!(err.is_not_found());
}
#[test]
fn is_not_found_nfs4() {
let err = NfsError::Nfs4(crate::nfs41::Nfs4ErrorCode::NFS4ERR_NOENT);
assert!(err.is_not_found());
}
#[test]
fn is_not_found_false_for_exist() {
let err = NfsError::Nfs4(crate::nfs41::Nfs4ErrorCode::NFS4ERR_EXIST);
assert!(!err.is_not_found());
}
#[test]
fn into_io_error_roundtrip() {
let nfs_err = NfsError::Rpc("test rpc error".to_string());
let io_err: std::io::Error = nfs_err.into();
assert_eq!(io_err.kind(), std::io::ErrorKind::Other);
assert!(io_err.to_string().contains("test rpc error"));
}
#[test]
fn into_io_error_preserves_io() {
let original = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken");
let nfs_err = NfsError::Io(original);
let io_err: std::io::Error = nfs_err.into();
assert_eq!(io_err.kind(), std::io::ErrorKind::BrokenPipe);
}
fn context() -> RequestContext {
RequestContext {
operation: "write".to_string(),
session_id: [7; 16],
slot_id: 3,
sequence_id: 9,
}
}
#[test]
fn sent_read_only_transport_failure_is_safe_to_retry() {
let error = classify_sent_nfs41_error(
OperationClass::ReadOnly,
context(),
NfsError::Io(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"reply timeout",
)),
);
let outcome = error
.operation_outcome()
.expect("outcome must be structured");
assert_eq!(outcome.outcome, OperationOutcome::SafeToRetry);
assert_eq!(outcome.recovery, RecoveryAction::Retry);
assert_eq!(outcome.context(), &context());
assert!(outcome.source().is_some());
}
#[test]
fn sent_modifying_transport_failure_is_uncertain() {
let error = classify_sent_nfs41_error(
OperationClass::ReplaySensitive,
context(),
NfsError::Io(std::io::Error::new(
std::io::ErrorKind::ConnectionReset,
"lost after send",
)),
);
let outcome = error
.operation_outcome()
.expect("outcome must be structured");
assert_eq!(outcome.outcome, OperationOutcome::Uncertain);
assert_eq!(outcome.recovery, RecoveryAction::VerifyThenResume);
assert_eq!(outcome.operation_class, OperationClass::ReplaySensitive);
}
#[test]
fn replay_protocol_errors_have_operation_aware_outcomes() {
for code in [
crate::nfs41::Nfs4ErrorCode::NFS4ERR_RETRY_UNCACHED_REP,
crate::nfs41::Nfs4ErrorCode::NFS4ERR_SEQ_FALSE_RETRY,
] {
let read = classify_sent_nfs41_error(
OperationClass::ReadOnly,
context(),
NfsError::Nfs4(code),
);
assert_eq!(
read.operation_outcome().map(|error| error.outcome),
Some(OperationOutcome::SafeToRetry)
);
let write = classify_sent_nfs41_error(
OperationClass::ReplaySensitive,
context(),
NfsError::Nfs4(code),
);
assert_eq!(
write.operation_outcome().map(|error| error.outcome),
Some(OperationOutcome::Uncertain)
);
}
}
#[test]
fn authoritative_protocol_failure_remains_definite_and_unwrapped() {
let error = classify_sent_nfs41_error(
OperationClass::ReplaySensitive,
context(),
NfsError::Nfs4(crate::nfs41::Nfs4ErrorCode::NFS4ERR_ACCESS),
);
assert!(error.operation_outcome().is_none());
assert!(matches!(
error,
NfsError::Nfs4(crate::nfs41::Nfs4ErrorCode::NFS4ERR_ACCESS)
));
}
#[test]
fn outcome_error_preserves_source_without_payload_context() {
let error = classify_sent_nfs41_error(
OperationClass::ReplaySensitive,
context(),
NfsError::Rpc("truncated authoritative reply".to_string()),
);
let outcome = error
.operation_outcome()
.expect("outcome must be structured");
assert!(
matches!(&*outcome.source, NfsError::Rpc(message) if message.contains("truncated"))
);
let debug = format!("{outcome:?}");
assert!(!debug.contains("file handle"));
assert!(!debug.contains("payload"));
}
}