Skip to content

Commit f04c0b4

Browse files
committed
fix(parser): handle include in hosts
Closes #118 Signed-off-by: Nathanael DEMACON <quantumsheep@users.noreply.github.com>
1 parent 52521e7 commit f04c0b4

4 files changed

Lines changed: 247 additions & 26 deletions

File tree

Cargo.lock

Lines changed: 63 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ strum = "0.26.3"
3232
strum_macros = "0.26.4"
3333
tui-input = "0.11.1"
3434
unicode-width = "0.2.0"
35+
36+
[dev-dependencies]
37+
tempdir = "0.3.7"

src/ssh_config/host_entry.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ use strum_macros;
44
#[derive(Debug, strum_macros::Display, strum_macros::EnumString, Eq, PartialEq, Hash, Clone)]
55
#[strum(ascii_case_insensitive)]
66
pub enum EntryType {
7-
#[strum(disabled)]
87
Unknown(String),
98
Host,
109
Match,

src/ssh_config/parser.rs

Lines changed: 181 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ impl Parser {
5858
}
5959

6060
fn parse_raw(&self, reader: &mut impl BufRead) -> Result<(Host, Vec<Host>), ParseError> {
61-
let mut global_host = Host::new(Vec::new());
62-
let mut is_in_host_block = false;
61+
let mut parent_host = Host::new(Vec::new());
6362
let mut hosts = Vec::new();
6463

6564
let mut line = String::new();
@@ -86,7 +85,6 @@ impl Parser {
8685
EntryType::Host => {
8786
let patterns = parse_patterns(&entry.1);
8887
hosts.push(Host::new(patterns));
89-
is_in_host_block = true;
9088

9189
continue;
9290
}
@@ -122,44 +120,33 @@ impl Parser {
122120
};
123121

124122
let mut file = BufReader::new(File::open(path)?);
125-
let (included_global_host, included_hosts) = self.parse_raw(&mut file)?;
126-
127-
if is_in_host_block {
128-
// Can't include hosts inside a host block
129-
if !included_hosts.is_empty() {
130-
return Err(InvalidIncludeError {
131-
line,
132-
details: InvalidIncludeErrorDetails::HostsInsideHostBlock,
133-
}
134-
.into());
135-
}
123+
let (included_parent_host, included_hosts) = self.parse_raw(&mut file)?;
136124

125+
if hosts.is_empty() {
126+
parent_host.extend_entries(&included_parent_host);
127+
} else {
137128
hosts
138129
.last_mut()
139130
.unwrap()
140-
.extend_entries(&included_global_host);
141-
} else {
142-
if !included_global_host.is_empty() {
143-
global_host.extend_entries(&included_global_host);
144-
}
145-
146-
hosts.extend(included_hosts);
131+
.extend_entries(&included_parent_host);
147132
}
133+
134+
hosts.extend(included_hosts);
148135
}
149136

150137
continue;
151138
}
152139
_ => {}
153140
}
154141

155-
if is_in_host_block {
156-
hosts.last_mut().unwrap().update(entry);
142+
if hosts.is_empty() {
143+
parent_host.update(entry);
157144
} else {
158-
global_host.update(entry);
145+
hosts.last_mut().unwrap().update(entry);
159146
}
160147
}
161148

162-
Ok((global_host, hosts))
149+
Ok((parent_host, hosts))
163150
}
164151
}
165152

@@ -218,3 +205,172 @@ fn parse_patterns(entry_value: &str) -> Vec<String> {
218205

219206
patterns
220207
}
208+
209+
#[cfg(test)]
210+
mod tests {
211+
use super::*;
212+
use std::fs::File;
213+
use std::io::{BufReader, Write};
214+
use tempdir::TempDir;
215+
216+
#[test]
217+
fn test_basic_host_parsing() {
218+
let config = r#"
219+
Host example
220+
User testuser
221+
Port 22
222+
"#;
223+
let mut reader = BufReader::new(config.as_bytes());
224+
let parser = Parser::new();
225+
let result = parser.parse(&mut reader).unwrap();
226+
227+
assert_eq!(result.len(), 1);
228+
let patterns = result[0].get_patterns();
229+
assert!(patterns.contains(&"example".to_string()));
230+
assert_eq!(result[0].get(&EntryType::User).unwrap(), "testuser");
231+
assert_eq!(result[0].get(&EntryType::Port).unwrap(), "22");
232+
}
233+
234+
#[test]
235+
fn test_global_settings_applied_to_all_hosts() {
236+
let config = r#"
237+
User globaluser
238+
239+
Host server1
240+
Port 22
241+
242+
Host server2
243+
Port 2200
244+
"#;
245+
let mut reader = BufReader::new(config.as_bytes());
246+
let parser = Parser::new();
247+
let result = parser.parse(&mut reader).unwrap();
248+
249+
assert_eq!(result.len(), 2);
250+
for host in result {
251+
assert_eq!(host.get(&EntryType::User).unwrap(), "globaluser");
252+
}
253+
}
254+
255+
#[test]
256+
fn test_include_file_parsing() {
257+
let include_content = r#"
258+
Host included
259+
Port 2222
260+
"#;
261+
262+
let temp_dir = TempDir::new("sshs").unwrap();
263+
let temp_file_path = temp_dir.path().join("included_config");
264+
let mut temp_file = File::create(&temp_file_path).unwrap();
265+
write!(temp_file, "{}", include_content).unwrap();
266+
267+
let config = format!(
268+
r#"
269+
Include {}
270+
Host main
271+
Port 22
272+
"#,
273+
temp_file_path.display()
274+
);
275+
276+
let mut reader = BufReader::new(config.as_bytes());
277+
let parser = Parser::new();
278+
let result = parser.parse(&mut reader).unwrap();
279+
280+
assert_eq!(result.len(), 2);
281+
let all_patterns: Vec<String> = result
282+
.iter()
283+
.flat_map(|host| host.get_patterns())
284+
.cloned()
285+
.collect();
286+
assert!(all_patterns.contains(&"included".to_string()));
287+
assert!(all_patterns.contains(&"main".to_string()));
288+
}
289+
290+
#[test]
291+
fn test_unknown_entry_error_when_not_ignored() {
292+
let config = r#"
293+
BogusEntry something
294+
Host test
295+
Port 22
296+
"#;
297+
let mut reader = BufReader::new(config.as_bytes());
298+
let mut parser = Parser::new();
299+
parser.ignore_unknown_entries = false;
300+
301+
let result = parser.parse(&mut reader);
302+
assert!(result.is_err());
303+
assert!(matches!(result.unwrap_err(), ParseError::UnknownEntry(_)));
304+
}
305+
306+
#[test]
307+
fn test_unknown_entry_ignored_when_flag_set() {
308+
let config = r#"
309+
BogusEntry something
310+
Host test
311+
Port 22
312+
"#;
313+
let mut reader = BufReader::new(config.as_bytes());
314+
let parser = Parser::new();
315+
316+
let result = parser.parse(&mut reader);
317+
assert!(result.is_ok());
318+
let hosts = result.unwrap();
319+
assert_eq!(hosts.len(), 1);
320+
}
321+
322+
#[test]
323+
fn test_comment_lines_ignored() {
324+
let config = r#"
325+
# This is a comment
326+
Host test # trailing comment
327+
User testuser # inline comment
328+
"#;
329+
let mut reader = BufReader::new(config.as_bytes());
330+
let parser = Parser::new();
331+
332+
let result = parser.parse(&mut reader).unwrap();
333+
assert_eq!(result.len(), 1);
334+
assert_eq!(result[0].get(&EntryType::User).unwrap(), "testuser");
335+
}
336+
337+
#[test]
338+
fn test_unparseable_line_error() {
339+
let config = r#"
340+
UnparseableLineWithoutValue
341+
"#;
342+
let mut reader = BufReader::new(config.as_bytes());
343+
let parser = Parser::new();
344+
345+
let result = parser.parse(&mut reader);
346+
assert!(matches!(
347+
result.unwrap_err(),
348+
ParseError::UnparseableLine(_)
349+
));
350+
}
351+
352+
#[test]
353+
fn test_parse_patterns_handles_quotes() {
354+
let patterns = parse_patterns(r#""host one" host2 "host three""#);
355+
assert_eq!(patterns, vec!["host one", "host2", "host three"]);
356+
}
357+
358+
#[test]
359+
fn test_parse_file_from_path() {
360+
let content = r#"
361+
Host fromfile
362+
Port 2222
363+
"#;
364+
365+
let temp_dir = TempDir::new("sshs").unwrap();
366+
let temp_file_path = temp_dir.path().join("included_config");
367+
let mut temp_file = File::create(&temp_file_path).unwrap();
368+
write!(temp_file, "{}", content).unwrap();
369+
370+
let parser = Parser::new();
371+
let result = parser.parse_file(temp_file_path).unwrap();
372+
373+
assert_eq!(result.len(), 1);
374+
assert!(result[0].get_patterns().contains(&"fromfile".to_string()));
375+
}
376+
}

0 commit comments

Comments
 (0)