use std::ffi::CStr;
use std::fmt;
use std::io;
use wolfcrypt_sys::*;
pub type Result<T> = std::result::Result<T, TlsError>;
#[derive(Debug)]
#[non_exhaustive]
pub enum TlsError {
InvalidConfig(&'static str),
CertificateVerification(String),
Io(io::Error),
AllocFailed { func: &'static str },
Ffi { code: i32, func: &'static str },
Closed,
}
impl fmt::Display for TlsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TlsError::InvalidConfig(msg) => write!(f, "invalid TLS config: {msg}"),
TlsError::CertificateVerification(msg) => {
write!(f, "certificate verification failed: {msg}")
}
TlsError::AllocFailed { func } => {
write!(f, "{func} returned NULL (allocation failed)")
}
TlsError::Io(err) => write!(f, "I/O error: {err}"),
TlsError::Ffi { code, func } => {
let reason = error_string(*code);
write!(f, "{func} failed: {reason} (wolfSSL error {code})")
}
TlsError::Closed => write!(f, "connection closed"),
}
}
}
impl std::error::Error for TlsError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
TlsError::Io(err) => Some(err),
_ => None,
}
}
}
impl From<io::Error> for TlsError {
fn from(err: io::Error) -> Self {
TlsError::Io(err)
}
}
pub(crate) fn error_string(code: core::ffi::c_int) -> &'static str {
let ptr = unsafe {
wolfSSL_ERR_reason_error_string((code as core::ffi::c_uint) as core::ffi::c_ulong)
};
if !ptr.is_null() {
if let Ok(s) = unsafe { CStr::from_ptr(ptr) }.to_str() {
if !s.is_empty() {
return s;
}
}
}
let ptr = unsafe { wc_GetErrorString(code) };
if !ptr.is_null() {
if let Ok(s) = unsafe { CStr::from_ptr(ptr) }.to_str() {
if !s.is_empty() {
return s;
}
}
}
"unknown error"
}
pub(crate) fn verify_error_string(code: core::ffi::c_long) -> &'static str {
let ptr = unsafe { wolfSSL_X509_verify_cert_error_string(code) };
if !ptr.is_null() {
if let Ok(s) = unsafe { CStr::from_ptr(ptr) }.to_str() {
if !s.is_empty() {
return s;
}
}
}
"unknown verification error"
}
pub(crate) fn expect_wolfssl_success(
ret: core::ffi::c_int,
func: &'static str,
) -> Result<core::ffi::c_int> {
if ret == WOLFSSL_SUCCESS as core::ffi::c_int {
Ok(ret)
} else {
Err(TlsError::Ffi { code: ret, func })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_produces_useful_messages() {
let err = TlsError::Ffi {
code: -308,
func: "wolfSSL_connect",
};
let msg = format!("{err}");
assert!(msg.contains("wolfSSL_connect"));
assert!(msg.contains("-308"));
let err = TlsError::CertificateVerification("expired".into());
assert!(format!("{err}").contains("expired"));
let err = TlsError::Closed;
assert!(format!("{err}").contains("closed"));
}
#[test]
fn ffi_error_includes_human_readable_reason() {
let err = TlsError::Ffi {
code: -188,
func: "wolfSSL_connect",
};
let msg = format!("{err}");
assert!(msg.contains("-188"), "should contain the error code");
assert!(
msg.contains("wolfSSL_connect"),
"should contain the function name"
);
let reason = error_string(-188);
assert!(
!reason.is_empty() && reason != "unknown error",
"error_string(-188) should return a known error, got: {reason:?}"
);
assert!(
msg.contains(reason),
"Display should include the reason string '{reason}', got: {msg}"
);
}
#[test]
fn error_string_returns_nonempty_for_known_codes() {
for code in [-155, -188, -245, -313] {
let s = error_string(code);
assert!(
!s.is_empty() && s != "unknown error",
"error_string({code}) should return a known error, got: {s:?}"
);
}
}
#[test]
fn verify_error_string_returns_useful_text() {
let ok_str = verify_error_string(0);
assert!(
!ok_str.is_empty() && ok_str != "unknown verification error",
"verify_error_string(0) should be recognised, got: {ok_str:?}"
);
let err_str = verify_error_string(20);
assert!(
!err_str.is_empty() && err_str != "unknown verification error",
"verify_error_string(20) should be recognised, got: {err_str:?}"
);
}
#[test]
fn error_code_cast_zero_extends_not_sign_extends() {
let code: core::ffi::c_int = -308;
let as_culong = (code as core::ffi::c_uint) as core::ffi::c_ulong;
assert_eq!(
as_culong >> 32,
0,
"error code should be zero-extended, not sign-extended; \
got {as_culong:#018x}"
);
assert_eq!(as_culong as u32, code as u32);
let s = error_string(code);
assert!(
s != "unknown error",
"error_string({code}) returned 'unknown error'; \
the zero-extension cast may be wrong"
);
}
#[test]
fn alloc_failed_display_says_allocation_failed() {
let err = TlsError::AllocFailed {
func: "wolfSSL_CTX_new",
};
let msg = format!("{err}");
assert!(
msg.contains("allocation failed"),
"AllocFailed should say 'allocation failed', got: {msg}"
);
assert!(
!msg.contains("out of memory"),
"AllocFailed should not say 'out of memory', got: {msg}"
);
assert!(msg.contains("wolfSSL_CTX_new"));
}
#[test]
fn from_io_error_roundtrips() {
let io_err = io::Error::new(io::ErrorKind::ConnectionRefused, "refused");
let tls_err = TlsError::from(io_err);
match &tls_err {
TlsError::Io(e) => assert_eq!(e.kind(), io::ErrorKind::ConnectionRefused),
other => panic!("expected Io variant, got: {other:?}"),
}
assert!(std::error::Error::source(&tls_err).is_some());
}
}