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}