Files
markbase/vendor/smb2/src/error.rs
Warren 7eb528d35f
Some checks failed
Test / build (push) Has been cancelled
Test / test (push) Has been cancelled
SMB Server Phase 2: VFS backend build fix + integration test
- Add VfsFile: Send supertrait for Mutex compatibility
- Fix SmbServerCommand: struct → Subcommand enum with Start variant
- Fix tracing_subscriber::init() → try_init() to avoid panic when
  logger already initialized
- Fix CLI subcommand name: smb-server → smb-start (flatten naming)
- Add #[command(name = "smb-start")] for CLI disambiguation
- Fix unused variable warnings (smb_fs.rs, smb_server_backend.rs)
- Remove unused VfsFile imports (webdav.rs, scp_handler.rs)
- Integration test: Docker smbclient verified (list, upload, read)
2026-06-20 19:42:29 +08:00

390 lines
14 KiB
Rust

//! Error types for the SMB2 library.
use crate::types::status::NtStatus;
use crate::types::Command;
use thiserror::Error;
/// Top-level error type for SMB2 operations.
#[derive(Debug, Error)]
pub enum Error {
/// The data is malformed or does not match the expected format.
#[error("Invalid data: {message}")]
InvalidData {
/// Description of what went wrong.
message: String,
},
/// The server returned a non-success NTSTATUS.
#[error("Protocol error: {status} during {command:?}")]
Protocol {
/// The NTSTATUS code from the response header.
status: NtStatus,
/// The command that triggered the error.
command: Command,
},
/// Authentication failed.
#[error("Authentication failed: {message}")]
Auth {
/// Description of what went wrong.
message: String,
},
/// An I/O or transport error occurred.
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
/// The operation timed out.
#[error("Operation timed out")]
Timeout,
/// The connection was lost.
#[error("Disconnected from server")]
Disconnected,
/// The path requires DFS referral resolution.
///
/// The server returned `STATUS_PATH_NOT_COVERED`, meaning this path
/// lives on a different server via DFS. The caller can query for a
/// referral or display a helpful message.
#[error("DFS referral required for path: {path}")]
DfsReferralRequired {
/// The path that needs DFS resolution.
path: String,
},
/// The operation was cancelled by the caller (via progress callback).
#[error("Operation cancelled")]
Cancelled,
/// The session expired and reauthentication failed.
///
/// The pipeline normally handles `STATUS_NETWORK_SESSION_EXPIRED`
/// transparently by reauthenticating. This error surfaces only
/// when reauthentication itself fails.
#[error("Session expired and reauthentication failed")]
SessionExpired,
}
impl Error {
/// Create an `InvalidData` error with the given message.
pub fn invalid_data(msg: impl Into<String>) -> Self {
Error::InvalidData {
message: msg.into(),
}
}
/// Returns `true` if this error is potentially transient and
/// the operation could succeed on retry.
pub fn is_retryable(&self) -> bool {
matches!(
self,
Error::Timeout
| Error::Disconnected
| Error::Protocol {
status: NtStatus::INSUFFICIENT_RESOURCES,
..
}
| Error::Protocol {
status: NtStatus::INSUFF_SERVER_RESOURCES,
..
}
)
}
/// Returns the NTSTATUS code if this is a protocol error.
pub fn status(&self) -> Option<NtStatus> {
match self {
Error::Protocol { status, .. } => Some(*status),
_ => None,
}
}
}
/// High-level error classification.
///
/// Maps protocol-level NTSTATUS codes and other errors into categories
/// that consumers can match on without understanding SMB internals.
///
/// ```no_run
/// # async fn example(client: &mut smb2::SmbClient, share: &mut smb2::Tree) -> Result<(), smb2::Error> {
/// use smb2::ErrorKind;
///
/// match client.read_file(share, "photo.jpg").await {
/// Ok(data) => println!("read {} bytes", data.len()),
/// Err(e) => match e.kind() {
/// ErrorKind::NotFound => println!("file doesn't exist"),
/// ErrorKind::AlreadyExists => println!("name is already taken"),
/// ErrorKind::AccessDenied => println!("no permission"),
/// ErrorKind::SigningRequired => println!("server requires signing, use credentials"),
/// ErrorKind::AuthRequired => println!("server requires authentication"),
/// ErrorKind::SharingViolation => println!("file is in use by another client"),
/// ErrorKind::IsADirectory => println!("path is a directory, not a file"),
/// ErrorKind::NotADirectory => println!("path is a file, not a directory"),
/// ErrorKind::DiskFull => println!("volume is full"),
/// ErrorKind::ConnectionLost => { client.reconnect().await?; }
/// _ => return Err(e),
/// }
/// }
/// # Ok(())
/// # }
/// ```
///
/// # Stability
///
/// `ErrorKind` is `#[non_exhaustive]`: future versions may add variants for
/// status codes that currently fall through to [`ErrorKind::Other`]. Match
/// statements should always include a `_` arm. Adding a variant is treated
/// as a non-breaking change.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorKind {
/// The server requires authentication (guest/anonymous not allowed).
AuthRequired,
/// The server requires message signing (guest sessions are unsigned).
SigningRequired,
/// Permission denied (valid credentials, but no access to this resource).
AccessDenied,
/// The file, directory, or share was not found.
NotFound,
/// A file or directory with the given name already exists.
///
/// Returned by `Create` (and operations that wrap it, like `create_directory`)
/// when the target name is taken. Useful for callers that want to merge into
/// an existing directory or surface a friendly "name already taken" message.
AlreadyExists,
/// The file is in use by another client.
SharingViolation,
/// The target path is a directory, but the operation expected a file.
///
/// Typically seen when calling `delete_file` against a directory entry —
/// the caller can fall back to `delete_directory` after detecting this.
IsADirectory,
/// The target path is a file, but the operation expected a directory.
///
/// Typically seen when calling `list_directory` against a file entry.
NotADirectory,
/// The volume is full (write failed).
DiskFull,
/// The network connection was lost.
ConnectionLost,
/// The operation timed out.
TimedOut,
/// The operation was cancelled by the caller.
Cancelled,
/// The session expired (call `reconnect()`).
SessionExpired,
/// The path requires DFS referral resolution.
DfsReferral,
/// Invalid data or malformed response.
InvalidData,
/// An I/O error (transport or callback). Not necessarily a connection loss.
///
/// Distinct from `ConnectionLost`: the connection may still be usable.
/// For example, a callback error in `write_file_streamed` produces `Io`,
/// but the connection is still in a clean state.
Io,
/// A protocol error not covered by other variants.
///
/// Use [`Error::status()`] to get the raw NTSTATUS code. Some defined
/// `NtStatus` codes deliberately fall through here today
/// (`OBJECT_NAME_INVALID`, `DELETE_PENDING`, `INSUFFICIENT_RESOURCES`,
/// `INSUFF_SERVER_RESOURCES`, and similar) — they don't yet have a
/// dedicated `ErrorKind` because no consumer needs to branch on them.
/// Promoting one to its own variant is non-breaking.
Other,
}
impl Error {
/// Classify this error into a high-level category.
///
/// Consumers can match on [`ErrorKind`] without understanding raw
/// NTSTATUS codes. For the underlying status code, use [`status()`](Self::status).
pub fn kind(&self) -> ErrorKind {
match self {
Error::InvalidData { .. } => ErrorKind::InvalidData,
Error::Auth { .. } => ErrorKind::AuthRequired,
Error::Io(_) => ErrorKind::Io,
Error::Disconnected => ErrorKind::ConnectionLost,
Error::Timeout => ErrorKind::TimedOut,
Error::Cancelled => ErrorKind::Cancelled,
Error::SessionExpired => ErrorKind::SessionExpired,
Error::DfsReferralRequired { .. } => ErrorKind::DfsReferral,
Error::Protocol { status, .. } => classify_status(*status),
}
}
}
/// Map an NTSTATUS to an ErrorKind.
fn classify_status(status: NtStatus) -> ErrorKind {
match status {
// Auth / signing
NtStatus::LOGON_FAILURE | NtStatus::ACCOUNT_DISABLED => ErrorKind::AuthRequired,
NtStatus::ACCESS_DENIED => {
// Could be signing-required or genuinely access-denied.
// Callers with NegotiatedParams context can distinguish further.
// Default to AccessDenied; SmbClient methods can upgrade to
// SigningRequired when signing_required is true.
ErrorKind::AccessDenied
}
// Not found
NtStatus::NO_SUCH_FILE
| NtStatus::OBJECT_NAME_NOT_FOUND
| NtStatus::OBJECT_PATH_NOT_FOUND
| NtStatus::BAD_NETWORK_NAME => ErrorKind::NotFound,
// Already exists
NtStatus::OBJECT_NAME_COLLISION => ErrorKind::AlreadyExists,
// Wrong file type
NtStatus::FILE_IS_A_DIRECTORY => ErrorKind::IsADirectory,
NtStatus::NOT_A_DIRECTORY => ErrorKind::NotADirectory,
// Sharing / locking
NtStatus::SHARING_VIOLATION | NtStatus::FILE_LOCK_CONFLICT => ErrorKind::SharingViolation,
// Disk full
NtStatus::DISK_FULL => ErrorKind::DiskFull,
// Session expired
NtStatus::NETWORK_SESSION_EXPIRED => ErrorKind::SessionExpired,
// Connection
NtStatus::NETWORK_NAME_DELETED | NtStatus::USER_SESSION_DELETED => {
ErrorKind::ConnectionLost
}
// DFS
NtStatus::PATH_NOT_COVERED => ErrorKind::DfsReferral,
// Everything else
_ => ErrorKind::Other,
}
}
/// A `Result` type alias using the crate's [`Error`](enum@Error) type.
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
/// Documents the full contract between `NtStatus` codes and `ErrorKind`.
///
/// Every code listed here is asserted to map to its expected variant. When
/// adding a new `NtStatus` to `types/status.rs`, also add a row here — either
/// pointing at a dedicated `ErrorKind`, or `ErrorKind::Other` if there is
/// genuinely no consumer-meaningful classification yet. The companion test
/// `classify_status_no_silent_other` then guarantees the table stays in sync
/// with what `classify_status` actually does.
const STATUS_CLASSIFICATION_CONTRACT: &[(NtStatus, ErrorKind)] = &[
// Auth / signing
(NtStatus::LOGON_FAILURE, ErrorKind::AuthRequired),
(NtStatus::ACCOUNT_DISABLED, ErrorKind::AuthRequired),
(NtStatus::ACCESS_DENIED, ErrorKind::AccessDenied),
// Not found
(NtStatus::NO_SUCH_FILE, ErrorKind::NotFound),
(NtStatus::OBJECT_NAME_NOT_FOUND, ErrorKind::NotFound),
(NtStatus::OBJECT_PATH_NOT_FOUND, ErrorKind::NotFound),
(NtStatus::BAD_NETWORK_NAME, ErrorKind::NotFound),
// Already exists
(NtStatus::OBJECT_NAME_COLLISION, ErrorKind::AlreadyExists),
// Wrong file type
(NtStatus::FILE_IS_A_DIRECTORY, ErrorKind::IsADirectory),
(NtStatus::NOT_A_DIRECTORY, ErrorKind::NotADirectory),
// Sharing / locking
(NtStatus::SHARING_VIOLATION, ErrorKind::SharingViolation),
(NtStatus::FILE_LOCK_CONFLICT, ErrorKind::SharingViolation),
// Disk
(NtStatus::DISK_FULL, ErrorKind::DiskFull),
// Connection / session
(NtStatus::NETWORK_NAME_DELETED, ErrorKind::ConnectionLost),
(NtStatus::USER_SESSION_DELETED, ErrorKind::ConnectionLost),
(NtStatus::NETWORK_SESSION_EXPIRED, ErrorKind::SessionExpired),
// DFS
(NtStatus::PATH_NOT_COVERED, ErrorKind::DfsReferral),
// Documented `Other` (no current consumer demand for a typed variant)
(NtStatus::NOT_IMPLEMENTED, ErrorKind::Other),
(NtStatus::INVALID_PARAMETER, ErrorKind::Other),
(NtStatus::DELETE_PENDING, ErrorKind::Other),
(NtStatus::INSUFFICIENT_RESOURCES, ErrorKind::Other),
(NtStatus::INSUFF_SERVER_RESOURCES, ErrorKind::Other),
];
#[test]
fn classify_status_contract() {
for (status, expected) in STATUS_CLASSIFICATION_CONTRACT {
let err = Error::Protocol {
status: *status,
command: Command::Create,
};
assert_eq!(
err.kind(),
*expected,
"{status} should classify as {expected:?}"
);
}
}
#[test]
fn kind_maps_non_protocol_errors() {
assert_eq!(Error::Timeout.kind(), ErrorKind::TimedOut);
assert_eq!(Error::Disconnected.kind(), ErrorKind::ConnectionLost);
assert_eq!(Error::Cancelled.kind(), ErrorKind::Cancelled);
assert_eq!(Error::SessionExpired.kind(), ErrorKind::SessionExpired);
assert_eq!(Error::invalid_data("test").kind(), ErrorKind::InvalidData);
assert_eq!(
Error::DfsReferralRequired {
path: "test".into()
}
.kind(),
ErrorKind::DfsReferral
);
assert_eq!(
Error::Auth {
message: "test".into()
}
.kind(),
ErrorKind::AuthRequired
);
}
#[test]
fn kind_maps_io_error_to_io_not_connection_lost() {
// Error::Io from callback errors (like write_file_streamed cancellation)
// should NOT be ConnectionLost — the connection may still be usable.
let err = Error::Io(std::io::Error::new(
std::io::ErrorKind::Interrupted,
"cancelled",
));
assert_eq!(err.kind(), ErrorKind::Io);
assert_ne!(err.kind(), ErrorKind::ConnectionLost);
}
#[test]
fn kind_disconnected_is_connection_lost() {
// Error::Disconnected (transport EOF) IS a connection loss.
assert_eq!(Error::Disconnected.kind(), ErrorKind::ConnectionLost);
}
#[test]
fn kind_maps_dfs_referral_required_to_dfs_referral() {
// The explicit DFS referral error variant should also map to DfsReferral.
let err = Error::DfsReferralRequired {
path: r"\\server\share\path".into(),
};
assert_eq!(err.kind(), ErrorKind::DfsReferral);
}
#[test]
fn dfs_referral_is_not_retryable() {
// DFS referrals need special handling, not generic retry.
let err = Error::Protocol {
status: NtStatus::PATH_NOT_COVERED,
command: Command::Create,
};
assert!(!err.is_retryable());
}
}