use std::collections::HashMap;
use std::convert::From;
use std::fmt;
use std::string::FromUtf8Error;
use thiserror::Error;
use super::Status;
pub type FtpResult<T> = std::result::Result<T, FtpError>;
#[derive(Debug, Error)]
pub enum FtpError {
#[error("Connection error: {0}")]
ConnectionError(std::io::Error),
#[cfg(any(feature = "secure", feature = "async-secure"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "secure", feature = "async-secure"))))]
#[error("Secure error: {0}")]
SecureError(String),
#[error("Invalid response: {0}")]
UnexpectedResponse(Response),
#[error("Response contains an invalid syntax")]
BadResponse,
#[error("Invalid address: {0}")]
InvalidAddress(std::net::AddrParseError),
#[error("Data connection is already open")]
DataConnectionAlreadyOpen,
}
#[derive(Clone, Debug, Error)]
pub struct Response {
pub status: Status,
pub body: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum FormatControl {
Default,
NonPrint,
Telnet,
Asa,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum FileType {
Ascii(FormatControl),
Ebcdic(FormatControl),
Image,
Binary,
Local(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Active,
ExtendedPassive,
Passive,
}
pub type Features = HashMap<String, Option<String>>;
impl fmt::Display for Response {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}] {}",
self.status.code(),
self.as_string().ok().unwrap_or_default()
)
}
}
impl Response {
pub fn new(status: Status, body: Vec<u8>) -> Self {
Self { status, body }
}
pub fn as_string(&self) -> Result<String, FromUtf8Error> {
String::from_utf8(self.body.clone()).map(|x| x.trim_end().to_string())
}
}
impl fmt::Display for FormatControl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
FormatControl::Default | FormatControl::NonPrint => String::from("N"),
FormatControl::Telnet => String::from("T"),
FormatControl::Asa => String::from("C"),
}
)
}
}
impl fmt::Display for FileType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
FileType::Ascii(fc) => format!("A {}", fc),
FileType::Ebcdic(fc) => format!("E {}", fc),
FileType::Image | FileType::Binary => String::from("I"),
FileType::Local(bits) => format!("L {bits}"),
}
)
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn fmt_error() {
assert_eq!(
FtpError::ConnectionError(std::io::Error::new(std::io::ErrorKind::NotFound, "omar"))
.to_string()
.as_str(),
"Connection error: omar"
);
#[cfg(feature = "secure")]
assert_eq!(
FtpError::SecureError("omar".to_string())
.to_string()
.as_str(),
"Secure error: omar"
);
assert_eq!(
FtpError::UnexpectedResponse(Response::new(
Status::ExceededStorage,
"error".as_bytes().to_vec()
))
.to_string()
.as_str(),
"Invalid response: [552] error"
);
assert_eq!(
FtpError::BadResponse.to_string().as_str(),
"Response contains an invalid syntax"
);
assert_eq!(
FtpError::InvalidAddress("127.0.0.1:abc".parse::<std::net::SocketAddr>().unwrap_err())
.to_string()
.as_str(),
"Invalid address: invalid socket address syntax"
);
assert_eq!(
FtpError::DataConnectionAlreadyOpen.to_string().as_str(),
"Data connection is already open"
);
}
#[test]
fn response() {
let response: Response = Response::new(Status::AboutToSend, "error".as_bytes().to_vec());
assert_eq!(response.status, Status::AboutToSend);
assert_eq!(response.as_string().unwrap(), "error");
}
#[test]
fn fmt_response() {
let response: Response = Response::new(
Status::FileUnavailable,
"Can't create directory: File exists".as_bytes().to_vec(),
);
assert_eq!(
response.to_string().as_str(),
"[550] Can't create directory: File exists"
);
}
#[test]
fn response_as_string_with_invalid_utf8() {
let response = Response::new(Status::CommandOk, vec![0xff, 0xfe, 0xfd]);
assert!(response.as_string().is_err());
}
#[test]
fn response_as_string_trims_trailing_whitespace() {
let response = Response::new(Status::CommandOk, "hello world \r\n".as_bytes().to_vec());
assert_eq!(response.as_string().unwrap(), "hello world");
}
#[test]
fn response_empty_body() {
let response = Response::new(Status::CommandOk, vec![]);
assert_eq!(response.as_string().unwrap(), "");
assert_eq!(response.to_string(), "[200] ");
}
#[test]
fn mode_debug() {
assert_eq!(format!("{:?}", Mode::Active), "Active");
assert_eq!(format!("{:?}", Mode::Passive), "Passive");
assert_eq!(format!("{:?}", Mode::ExtendedPassive), "ExtendedPassive");
}
#[test]
fn mode_clone_and_eq() {
let mode = Mode::Passive;
let cloned = mode;
assert_eq!(mode, cloned);
assert_ne!(Mode::Active, Mode::Passive);
assert_ne!(Mode::ExtendedPassive, Mode::Passive);
}
#[test]
fn file_type_clone_and_eq() {
let ft = FileType::Binary;
let cloned = ft.clone();
assert_eq!(ft, cloned);
assert_ne!(FileType::Binary, FileType::Ascii(FormatControl::Default));
assert_ne!(FileType::Image, FileType::Local(8));
}
#[test]
fn format_control_ordering() {
assert!(FormatControl::Default < FormatControl::NonPrint);
assert!(FormatControl::NonPrint < FormatControl::Telnet);
assert!(FormatControl::Telnet < FormatControl::Asa);
}
#[test]
fn fmt_format_control() {
assert_eq!(FormatControl::Asa.to_string().as_str(), "C");
assert_eq!(FormatControl::Telnet.to_string().as_str(), "T");
assert_eq!(FormatControl::Default.to_string().as_str(), "N");
assert_eq!(FormatControl::NonPrint.to_string().as_str(), "N");
}
#[test]
fn fmt_file_type() {
assert_eq!(
FileType::Ascii(FormatControl::Telnet).to_string().as_str(),
"A T"
);
assert_eq!(FileType::Binary.to_string().as_str(), "I");
assert_eq!(FileType::Image.to_string().as_str(), "I");
assert_eq!(
FileType::Ebcdic(FormatControl::Telnet).to_string().as_str(),
"E T"
);
assert_eq!(FileType::Local(2).to_string().as_str(), "L 2");
}
}