1use std::{fs, path::{Path, PathBuf}};
2
3use crate::parser;
4
5pub fn get_path() -> String {
6 resolve_config_path(home::home_dir())
7}
8
9fn resolve_config_path(home: Option<PathBuf>) -> String {
13 let mut path = match home {
14 Some(p) => p.to_str().unwrap().to_string(),
15 None => {
16 println!("Impossible to get your home dir!");
17 String::new()
18 }
19 };
20 path.push_str("/.ssh/config");
21 if !Path::new(&path).exists() {
22 reset_config_file(&path);
23 }
24 path
25}
26
27fn reset_config_file(path: &str) {
28 match fs::write(path, "") {
29 Ok(_) => println!("Recreated config file!"),
30 Err(_) => ()
31 }
32}
33
34pub fn delete_all_command() {
35 let path = get_path();
36 reset_config_file(&path);
37}
38
39fn extract_host_name(host_item: &mut parser::HostItem, host_string: String) {
40 let mut user = String::from("root");
41 let mut hostname = String::from("");
42 let mut port = String::from("22");
43
44 let splitted: Vec<String> = host_string.split("@")
45 .map(|s| s.to_string())
46 .collect();
47
48 if splitted.len() == 1 {
49 hostname = String::from(&splitted[0]);
50 } else if splitted.len() == 2 {
51 user = String::from(&splitted[0]);
52 hostname = String::from(&splitted[1]);
53 }
54
55 let hostname_splitted: Vec<String> = hostname.split(":")
56 .map(|s| s.to_string())
57 .collect();
58
59 if hostname_splitted.len() == 2 {
60 hostname = String::from(&hostname_splitted[0]);
61 port = String::from(&hostname_splitted[1]);
62 }
63
64 host_item.user = user;
65 host_item.host = hostname;
66 host_item.port = port;
67}
68
69pub fn add_command(name: &String, host: &String, private_key: &Option<String>, port: &Option<String>) {
70 let path = get_path();
71
72 let mut new_host = parser::HostItem::new();
73 new_host.name = name.clone();
74 extract_host_name(&mut new_host, host.to_string());
75 if let Some(key) = private_key {
76 new_host.identity_file = key.clone();
77 }
78 if let Some(p) = port {
80 new_host.port = p.clone();
81 }
82 let mut config = parser::parse(&path);
83 config.hosts.push(new_host);
84 config.write(&path).unwrap();
85}
86
87pub fn edit_command(name: &String, host: &String, private_key: &Option<String>, port: &Option<String>) {
88 let path = get_path();
89 let mut config = parser::parse(&path);
90 let mut host_item = parser::HostItem::new();
91
92 extract_host_name(&mut host_item, host.to_string());
93 if let Some(key) = private_key {
94 host_item.identity_file = key.clone();
95 }
96 if let Some(p) = port {
98 host_item.port = p.clone();
99 }
100
101 match config.edit(name, &host_item) {
102 Ok(_) => config.write(&path).unwrap(),
103 Err(err) => eprintln!("Can not edit: {}", err)
104 }
105}
106
107pub fn rename_command(name: &String, host: &String) {
108 let path = get_path();
109 let mut config = parser::parse(&path);
110 match config.rename(name, host) {
111 Ok(_) => config.write(&path).unwrap(),
112 Err(err) => eprintln!("Can not rename: {}", err)
113 }
114}
115
116pub fn delete_command(name: &String) {
117 let path = get_path();
118 let mut config = parser::parse(&path);
119 match config.delete(name) {
120 Ok(_) => config.write(&path).unwrap(),
121 Err(err) => eprintln!("Can not delete: {}", err)
122 }
123}
124
125pub fn list_command() {
126 let path = get_path();
127 let config = parser::parse(&path);
128
129 if config.hosts.len() == 0 {
130 println!("No records");
131 } else {
132 config.log()
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::parser::HostItem;
140 use std::path::PathBuf;
141 use std::sync::atomic::{AtomicUsize, Ordering};
142
143 fn unique_tmp(tag: &str) -> PathBuf {
144 static COUNTER: AtomicUsize = AtomicUsize::new(0);
145 let n = COUNTER.fetch_add(1, Ordering::SeqCst);
146 let mut dir = std::env::temp_dir();
147 dir.push(format!("zoi_cmd_{}_{}_{}", tag, std::process::id(), n));
148 dir
149 }
150
151 #[test]
152 fn extract_hostname_only_uses_defaults() {
153 let mut host = HostItem::new();
154 extract_host_name(&mut host, "example.com".to_string());
155 assert_eq!(host.user, "root");
156 assert_eq!(host.host, "example.com");
157 assert_eq!(host.port, "22");
158 }
159
160 #[test]
161 fn extract_user_host_and_port() {
162 let mut host = HostItem::new();
163 extract_host_name(&mut host, "deploy@10.0.0.5:2222".to_string());
164 assert_eq!(host.user, "deploy");
165 assert_eq!(host.host, "10.0.0.5");
166 assert_eq!(host.port, "2222");
167 }
168
169 #[test]
170 fn extract_user_host_without_port_defaults_22() {
171 let mut host = HostItem::new();
172 extract_host_name(&mut host, "admin@myhost".to_string());
173 assert_eq!(host.user, "admin");
174 assert_eq!(host.host, "myhost");
175 assert_eq!(host.port, "22");
176 }
177
178 #[test]
179 fn extract_malformed_multiple_at_leaves_host_empty() {
180 let mut host = HostItem::new();
183 extract_host_name(&mut host, "a@b@c".to_string());
184 assert_eq!(host.user, "root");
185 assert_eq!(host.host, "");
186 assert_eq!(host.port, "22");
187 }
188
189 #[test]
190 fn resolve_path_creates_config_when_missing() {
191 let dir = unique_tmp("missing");
192 std::fs::create_dir_all(dir.join(".ssh")).unwrap();
193
194 let path = resolve_config_path(Some(dir.clone()));
195
196 assert_eq!(path, format!("{}/.ssh/config", dir.to_str().unwrap()));
197 assert!(Path::new(&path).exists());
198 assert_eq!(std::fs::read_to_string(&path).unwrap(), "");
199
200 std::fs::remove_dir_all(&dir).ok();
201 }
202
203 #[test]
204 fn resolve_path_keeps_existing_config() {
205 let dir = unique_tmp("existing");
206 let ssh = dir.join(".ssh");
207 std::fs::create_dir_all(&ssh).unwrap();
208 std::fs::write(ssh.join("config"), "Host x\nhostname y\nuser z\nport 1\n").unwrap();
209
210 let path = resolve_config_path(Some(dir.clone()));
211
212 assert!(std::fs::read_to_string(&path).unwrap().contains("Host x"));
214
215 std::fs::remove_dir_all(&dir).ok();
216 }
217
218 #[test]
219 fn resolve_path_without_home_does_not_panic() {
220 let path = resolve_config_path(None);
223 assert_eq!(path, "/.ssh/config");
224 }
225
226 #[test]
227 fn reset_config_file_truncates_and_ignores_bad_path() {
228 let dir = unique_tmp("reset");
229 std::fs::create_dir_all(&dir).unwrap();
230 let good = dir.join("cfg");
231 std::fs::write(&good, "some content").unwrap();
232
233 reset_config_file(good.to_str().unwrap());
234 assert_eq!(std::fs::read_to_string(&good).unwrap(), "");
235
236 reset_config_file("/zoi_nonexistent_dir_xyz/cfg");
238
239 std::fs::remove_dir_all(&dir).ok();
240 }
241}