@@ -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