Skip to main content

genja_core/settings/
ssh_loading.rs

1use super::SSHConfig;
2use crate::SshConfigError;
3use ssh2_config::{ParseRule, SshConfig};
4use std::fs::File as StdFile;
5use std::io::{BufReader, ErrorKind};
6use std::path::Path;
7
8impl SSHConfig {
9    /// Validates the SSH configuration file syntax if a path is provided.
10    ///
11    /// This method performs comprehensive validation of an SSH configuration file by:
12    /// 1. Verifying that the file exists and is accessible
13    /// 2. Opening the file for reading
14    /// 3. Parsing the file contents using strict SSH config syntax rules
15    ///
16    /// If no SSH configuration file is specified (the `config_file` field is `None`),
17    /// this method returns `Ok(())` without performing any validation.
18    ///
19    /// # Returns
20    ///
21    /// Returns `Ok(())` if:
22    /// * No config file is specified (nothing to validate)
23    /// * The config file exists, can be opened, and contains valid SSH configuration syntax
24    ///
25    /// Returns `Err(SshConfigError)` if:
26    /// * The specified file does not exist or cannot be accessed
27    /// * The file cannot be opened due to permission issues or other I/O errors
28    /// * The file contents cannot be parsed as valid SSH configuration syntax
29    ///
30    /// # Errors
31    ///
32    /// This method returns an error in the following cases:
33    /// * File existence check fails (see `ensure_exists` for details)
34    /// * `SshConfigError::OpenFailed` - The file exists but cannot be opened
35    /// * `SshConfigError::ParseFailed` - The file contains invalid SSH config syntax
36    ///
37    /// # Examples
38    ///
39    /// ```no_run
40    /// use genja_core::settings::SSHConfig;
41    ///
42    /// let config = SSHConfig::builder()
43    ///     .config_file("/home/user/.ssh/config")
44    ///     .build();
45    ///
46    /// match config.validate() {
47    ///     Ok(()) => println!("SSH config is valid"),
48    ///     Err(e) => eprintln!("Invalid SSH config: {}", e),
49    /// }
50    /// ```
51    pub fn validate(&self) -> Result<(), SshConfigError> {
52        if let Some(ref path) = self.config_file {
53            let path = Path::new(path);
54
55            self.ensure_exists(path)?;
56
57            let file = match StdFile::open(path) {
58                Ok(file) => file,
59                Err(e) => {
60                    return Err(SshConfigError::OpenFailed {
61                        path: path.display().to_string(),
62                        message: e.to_string(),
63                    });
64                }
65            };
66            let mut reader = BufReader::new(file);
67
68            match SshConfig::default().parse(&mut reader, ParseRule::STRICT) {
69                Ok(_) => Ok(()),
70                Err(e) => Err(SshConfigError::ParseFailed {
71                    path: path.display().to_string(),
72                    message: e.to_string(),
73                }),
74            }
75        } else {
76            Ok(())
77        }
78    }
79
80    /// Parses the SSH configuration file and returns the parsed configuration.
81    ///
82    /// This method reads and parses an SSH configuration file if one is specified in the
83    /// `config_file` field. The parsing follows strict SSH config file syntax rules as
84    /// defined by OpenSSH. If no configuration file is specified, the method returns
85    /// `Ok(None)` without performing any parsing.
86    ///
87    /// # Returns
88    ///
89    /// Returns a `Result` containing:
90    /// * `Ok(Some(SshConfig))` - If a config file is specified and successfully parsed,
91    ///   containing the parsed SSH configuration with all host entries and settings.
92    /// * `Ok(None)` - If no config file is specified (the `config_file` field is `None`).
93    /// * `Err(SshConfigError)` - If an error occurs during parsing.
94    ///
95    /// # Errors
96    ///
97    /// Returns [`SshConfigError`] if:
98    /// * The specified SSH config file does not exist at the given path
99    /// * The file cannot be opened due to permission issues or other I/O errors
100    /// * The file contents cannot be parsed as valid SSH configuration syntax
101    /// * The file contains syntax errors or invalid SSH configuration directives
102    ///
103    /// # Examples
104    ///
105    /// ```no_run
106    /// use genja_core::settings::SSHConfig;
107    ///
108    /// let config = SSHConfig::builder()
109    ///     .config_file("/home/user/.ssh/config")
110    ///     .build();
111    ///
112    /// match config.parse() {
113    ///     Ok(Some(ssh_config)) => {
114    ///         println!("Successfully parsed SSH config");
115    ///     }
116    ///     Ok(None) => {
117    ///         println!("No SSH config file specified");
118    ///     }
119    ///     Err(e) => {
120    ///         eprintln!("Failed to parse SSH config: {}", e);
121    ///     }
122    /// }
123    /// ```
124    pub fn parse(&self) -> Result<Option<SshConfig>, SshConfigError> {
125        if let Some(ref path) = self.config_file {
126            let path = Path::new(path);
127
128            self.ensure_exists(path)?;
129
130            let file = match StdFile::open(path) {
131                Ok(file) => file,
132                Err(e) => Err(SshConfigError::OpenFailed {
133                    path: path.display().to_string(),
134                    message: e.to_string(),
135                })?,
136            };
137            let mut reader = BufReader::new(file);
138
139            match SshConfig::default().parse(&mut reader, ParseRule::STRICT) {
140                Ok(config) => Ok(Some(config)),
141                Err(e) => Err(SshConfigError::ParseFailed {
142                    path: path.display().to_string(),
143                    message: e.to_string(),
144                }),
145            }
146        } else {
147            Ok(None)
148        }
149    }
150
151    /// Verifies that an SSH configuration file exists and is accessible.
152    ///
153    /// This method checks whether the specified file path exists and can be accessed.
154    /// It provides detailed error messages for different failure scenarios, including
155    /// permission issues and I/O errors.
156    ///
157    /// # Parameters
158    ///
159    /// * `path` - A reference to the file path to check. This should point to an SSH
160    ///   configuration file that needs to be validated for existence and accessibility.
161    ///
162    /// # Returns
163    ///
164    /// Returns `Ok(())` if the file exists and is accessible.
165    ///
166    /// Returns `Err(SshConfigError)` if:
167    /// * The file does not exist
168    /// * Permission is denied when attempting to access the file
169    /// * An I/O error occurs during the existence check
170    /// * Any other filesystem error prevents verification
171    ///
172    /// # Errors
173    ///
174    /// This method returns an error in the following cases:
175    /// * `SshConfigError::NotFound` - The file does not exist at the specified path
176    /// * `SshConfigError::PermissionDenied` - The file exists but cannot be accessed due to
177    ///   insufficient permissions
178    /// * `SshConfigError::CheckFailed` - Any other filesystem error occurred during the check
179    pub(super) fn ensure_exists(&self, path: &Path) -> Result<(), SshConfigError> {
180        match path.try_exists() {
181            Ok(true) => Ok(()),
182            Ok(false) => Err(SshConfigError::NotFound {
183                path: path.display().to_string(),
184            }),
185            Err(e) => match e.kind() {
186                ErrorKind::PermissionDenied => Err(SshConfigError::PermissionDenied {
187                    path: path.display().to_string(),
188                    message: e.to_string(),
189                }),
190                ErrorKind::NotFound => Err(SshConfigError::NotFound {
191                    path: path.display().to_string(),
192                }),
193                _ => Err(SshConfigError::CheckFailed {
194                    path: path.display().to_string(),
195                    message: e.to_string(),
196                }),
197            },
198        }
199    }
200}