@@ -146,7 +146,17 @@ pub fn pretty_print_known_hosts(response: &KnownHostsResponse) {
146146 ) ;
147147}
148148
149- pub fn fetch_known_hosts ( server_url : & str ) -> Result < ( ) > {
149+ /// Private function to fetch known hosts from the server
150+ ///
151+ /// This function handles the HTTP request to the known hosts server,
152+ /// validates the response, and parses the JSON into a KnownHostsResponse.
153+ ///
154+ /// # Arguments
155+ /// * `server_url` - The base URL of the keys server
156+ ///
157+ /// # Returns
158+ /// * `Result<KnownHostsResponse>` - The parsed known hosts response or an error
159+ fn fetch_known_hosts_from_server ( server_url : & str ) -> Result < KnownHostsResponse > {
150160 let url = format ! ( "{server_url}/known_hosts" ) ;
151161
152162 let client = reqwest:: blocking:: Client :: new ( ) ;
@@ -166,8 +176,13 @@ pub fn fetch_known_hosts(server_url: &str) -> Result<()> {
166176 ) ) ;
167177 }
168178
169- let known_hosts_response: KnownHostsResponse =
170- response. json ( ) . context ( "Failed to parse JSON response" ) ?;
179+ response
180+ . json :: < KnownHostsResponse > ( )
181+ . context ( "Failed to parse JSON response" )
182+ }
183+
184+ pub fn fetch_known_hosts ( server_url : & str ) -> Result < ( ) > {
185+ let known_hosts_response = fetch_known_hosts_from_server ( server_url) ?;
171186
172187 // Check if the output is being piped (not connected to a terminal)
173188 // Use raw/minimal output when piped to another command
@@ -218,6 +233,94 @@ pub fn fetch_known_hosts(server_url: &str) -> Result<()> {
218233 Ok ( ( ) )
219234}
220235
236+ /// Helper function to format a host entry in known_hosts format
237+ fn format_known_hosts_line ( host : & KnownHost , key : & HostKey ) -> String {
238+ let hosts_str = host. hosts . join ( "," ) ;
239+ let key_type = & key. key_type ;
240+ let key_value = & key. key ;
241+
242+ // Add flags if present
243+ let mut flags = Vec :: new ( ) ;
244+ if key. revoked . unwrap_or ( false ) {
245+ flags. push ( "@revoked" ) ;
246+ }
247+ if key. cert_authority . unwrap_or ( false ) {
248+ flags. push ( "@cert-authority" ) ;
249+ }
250+
251+ // Format comment if present
252+ let comment_str = if let Some ( comment) = & key. comment {
253+ format ! ( " # {comment}" )
254+ } else {
255+ String :: new ( )
256+ } ;
257+
258+ // Output in OpenSSH known_hosts format with flags and optional comment
259+ if flags. is_empty ( ) {
260+ format ! ( "{hosts_str} {key_type} {key_value}{comment_str}" )
261+ } else {
262+ format ! (
263+ "{} {} {} {}{}" ,
264+ flags. join( " " ) ,
265+ hosts_str,
266+ key_type,
267+ key_value,
268+ comment_str
269+ )
270+ }
271+ }
272+
273+ pub fn write_known_hosts ( server_url : & str , file_path : & str ) -> Result < ( ) > {
274+ // Fetch known hosts from the server
275+ let known_hosts_response = fetch_known_hosts_from_server ( server_url) ?;
276+
277+ // Expand ~ to home directory if present
278+ let expanded_path = shellexpand:: tilde ( file_path) ;
279+ let path = std:: path:: Path :: new ( expanded_path. as_ref ( ) ) ;
280+
281+ // Create directory if it doesn't exist
282+ if let Some ( parent) = path. parent ( )
283+ && !parent. exists ( )
284+ {
285+ std:: fs:: create_dir_all ( parent)
286+ . with_context ( || format ! ( "Failed to create parent directory: {}" , parent. display( ) ) ) ?;
287+ }
288+
289+ // Generate the file content (always replace completely)
290+ let mut lines = Vec :: new ( ) ;
291+ for host in & known_hosts_response. hosts {
292+ for key in & host. keys {
293+ lines. push ( format_known_hosts_line ( host, key) ) ;
294+ }
295+ }
296+
297+ let file_content = lines. join ( "\n " ) ;
298+ if !file_content. is_empty ( ) {
299+ let file_content = format ! ( "{}\n " , file_content) ;
300+ std:: fs:: write ( path, file_content)
301+ . with_context ( || format ! ( "Failed to write to file: {}" , path. display( ) ) ) ?;
302+ } else {
303+ // Write empty file if no hosts
304+ std:: fs:: write ( path, "" )
305+ . with_context ( || format ! ( "Failed to write to file: {}" , path. display( ) ) ) ?;
306+ }
307+
308+ // Count the total number of entries written
309+ let total_entries: usize = known_hosts_response
310+ . hosts
311+ . iter ( )
312+ . map ( |h| h. keys . len ( ) )
313+ . sum ( ) ;
314+
315+ println ! (
316+ "✅ Wrote {} known host entries to {}" ,
317+ total_entries,
318+ path. display( )
319+ ) ;
320+
321+ Ok ( ( ) )
322+ }
323+
221324#[ cfg( test) ]
222325mod tests {
223326 use super :: * ;
0 commit comments