use crate::config::ResolvedServer;
fn escape_field(value: &str) -> String {
if value.contains([',', '"', '\n', '\r']) {
format!("\"{}\"", value.replace('"', "\"\""))
} else {
value.to_string()
}
}
pub fn to_csv_string(servers: &[&ResolvedServer]) -> String {
let mut out = String::from("name,host,user,port,ssh_key,group,env,namespace,tags,notes\n");
for s in servers {
let tags = s.tags.join(";");
let row = [
escape_field(&s.name),
escape_field(&s.host),
escape_field(&s.user),
s.port.to_string(),
escape_field(&s.ssh_key),
escape_field(&s.group_name),
escape_field(&s.env_name),
escape_field(&s.namespace),
escape_field(&tags),
escape_field(&s.notes),
]
.join(",");
out.push_str(&row);
out.push('\n');
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{ConnectionMode, ResolvedServer};
fn make_server(name: &str, host: &str, group: &str, tags: Vec<&str>) -> ResolvedServer {
ResolvedServer {
name: name.into(),
host: host.into(),
user: "ops".into(),
port: 22,
ssh_key: String::new(),
group_name: group.into(),
env_name: String::new(),
namespace: String::new(),
tags: tags.into_iter().map(String::from).collect(),
notes: String::new(),
default_mode: ConnectionMode::Direct,
jump_host: None,
bastion_host: None,
bastion_user: None,
bastion_template: String::new(),
use_system_ssh_config: false,
probe_filesystems: vec![],
tunnels: vec![],
ssh_options: vec![],
control_master: false,
agent_forwarding: false,
control_path: String::new(),
control_persist: String::new(),
pre_connect_hook: None,
post_disconnect_hook: None,
hook_timeout_secs: 5,
ssh_cert: String::new(),
ssh_agent_sock: String::new(),
wallix_group: None,
wallix_account: String::new(),
wallix_protocol: String::new(),
wallix_auto_select: false,
wallix_fail_if_menu_match_error: false,
wallix_selection_timeout_secs: 8,
wallix_direct: false,
wallix_authorization: None,
wallix_header_columns: vec![],
}
}
#[test]
fn csv_header_present() {
let out = to_csv_string(&[]);
assert!(out.starts_with("name,host,user,port,ssh_key,group,env,namespace,tags,notes\n"));
}
#[test]
fn csv_basic_row() {
let s = make_server("api-01", "198.51.100.1", "prod", vec!["web", "prod"]);
let out = to_csv_string(&[&s]);
assert!(out.contains("api-01"), "name absent");
assert!(out.contains("198.51.100.1"), "host absent");
assert!(out.contains("ops"), "user absent");
assert!(out.contains(",22,"), "port absent");
assert!(out.contains("web;prod"), "tags absents");
}
#[test]
fn csv_escapes_commas_in_field() {
let mut s = make_server("db", "10.0.0.1", "grp", vec![]);
s.notes = "primary, high-traffic".into();
let out = to_csv_string(&[&s]);
assert!(
out.contains("\"primary, high-traffic\""),
"virgule doit être échappée"
);
}
#[test]
fn csv_escapes_quotes_in_field() {
let mut s = make_server("db", "10.0.0.1", "grp", vec![]);
s.notes = "say \"hello\"".into();
let out = to_csv_string(&[&s]);
assert!(
out.contains("\"say \"\"hello\"\"\""),
"guillemets doublés attendus"
);
}
}