use crate::FtpError;
use crate::types::Features;
pub fn parse_features(lines: &[String]) -> Result<Features, FtpError> {
let first_line = lines.first().ok_or(FtpError::BadResponse)?;
debug!("Parsing features; first line: {first_line}");
let mut features = Features::with_capacity(lines.len());
if first_line.starts_with("211-") {
debug!("Found `211-` - features available");
for line in lines.iter().skip(1) {
if line.starts_with("211 ") {
debug!("Found `211 End` - end of FEAT");
break;
}
parse_feature(line, &mut features)?;
}
Ok(features)
} else if first_line.starts_with("211 ") {
debug!("Found `211` - no features available");
Ok(features)
} else {
Err(FtpError::BadResponse)
}
}
fn parse_feature(line: &str, features: &mut Features) -> Result<(), FtpError> {
if !line.starts_with(' ') {
error!("Feature response doesn't start with ` `");
return Err(FtpError::BadResponse);
}
let mut line = line.trim().split(' ');
let Some(feature_name) = line.next() else {
error!("Feature line is empty");
return Err(FtpError::BadResponse);
};
let feature_values = match line.collect::<Vec<&str>>().join(" ") {
values if values.is_empty() => None,
values => Some(values),
};
debug!("found supported feature: {feature_name}: {feature_values:?}");
features.insert(feature_name.to_string(), feature_values);
Ok(())
}
pub fn is_last_line(line: &str) -> bool {
line.starts_with("211 ")
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_should_parse_no_features() {
let lines = vec!["211 No features available".to_string()];
let features = parse_features(&lines).expect("failed to parse features");
assert!(features.is_empty());
}
#[test]
fn test_should_parse_features() {
let lines = vec![
"211-Features:".to_string(),
" MLST size*;create;modify*;perm;media-type".to_string(),
" SIZE".to_string(),
" COMPRESSION".to_string(),
"211 END".to_string(),
];
let features = parse_features(&lines).expect("failed to parse features");
assert_eq!(features.len(), 3);
assert!(features.contains_key("MLST"));
assert_eq!(
features
.get("MLST")
.as_ref()
.expect("no MLST")
.as_deref()
.expect("no value for MLST"),
"size*;create;modify*;perm;media-type"
);
assert!(features.contains_key("SIZE"));
assert_eq!(features.get("SIZE"), Some(&None));
assert!(features.contains_key("COMPRESSION"));
assert_eq!(features.get("COMPRESSION"), Some(&None));
}
#[test]
fn test_is_last_line() {
assert!(is_last_line("211 END"));
assert!(is_last_line("211 "));
assert!(!is_last_line("211-Features:"));
assert!(!is_last_line(" MLST size*"));
assert!(!is_last_line("200 OK"));
}
#[test]
fn test_should_not_parse_empty_lines() {
let lines: Vec<String> = vec![];
let result = parse_features(&lines);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), FtpError::BadResponse));
}
#[test]
fn test_should_not_parse_invalid_features() {
let lines = vec![
"211-Features:".to_string(),
"Invalid feature line".to_string(),
];
let result = parse_features(&lines);
assert!(result.is_err(), "Expected error for invalid feature line");
assert!(matches!(result.unwrap_err(), FtpError::BadResponse));
}
#[test]
fn test_should_not_parse_unknown_first_line() {
let lines = vec!["500 Unknown".to_string()];
let result = parse_features(&lines);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), FtpError::BadResponse));
}
#[test]
fn test_should_parse_features_without_end_marker() {
let lines = vec![
"211-Features:".to_string(),
" UTF8".to_string(),
" MLST size*;modify*".to_string(),
];
let features = parse_features(&lines).expect("failed to parse features");
assert_eq!(features.len(), 2);
assert!(features.contains_key("UTF8"));
assert!(features.contains_key("MLST"));
}
#[test]
fn test_should_parse_feature_with_value() {
let lines = vec![
"211-Features:".to_string(),
" REST STREAM".to_string(),
"211 END".to_string(),
];
let features = parse_features(&lines).expect("failed to parse features");
assert_eq!(features.len(), 1);
assert_eq!(features.get("REST").unwrap().as_deref(), Some("STREAM"));
}
#[test]
fn test_is_last_line_edge_cases() {
assert!(!is_last_line(""));
assert!(!is_last_line("211"));
assert!(is_last_line("211 End"));
assert!(!is_last_line("2111 "));
}
}