1use thiserror::Error;
7
8#[derive(Debug, Error)]
10pub enum ErroSshCli {
11 #[error("erro de I/O: {0}")]
13 Io(#[from] std::io::Error),
14
15 #[error("erro de JSON: {0}")]
17 Json(#[from] serde_json::Error),
18
19 #[error("erro de TOML (leitura): {0}")]
21 TomlDe(#[from] toml::de::Error),
22
23 #[error("erro de TOML (escrita): {0}")]
25 TomlSer(#[from] toml::ser::Error),
26
27 #[error("erro de conexão SSH: {0}")]
29 ConexaoSsh(String),
30
31 #[error("erro de autenticação SSH: {0}")]
33 AutenticacaoSsh(String),
34
35 #[error("conexão SSH falhou: {0}")]
37 ConexaoFalhou(String),
38
39 #[error(
41 "autenticação SSH falhou; tente --password-stdin, --key PATH, --key-passphrase-stdin ou verifique o usuário"
42 )]
43 AutenticacaoFalhou,
44
45 #[error(
47 "host key mudou para {host}:{porta}: esperado {esperado}, obtido {obtido} (use --replace-host-key se a troca for legítima)"
48 )]
49 HostKeyMudou {
50 host: String,
52 porta: u16,
54 esperado: String,
56 obtido: String,
58 },
59
60 #[error("comando excede max_command_chars ({max}): {len} caracteres")]
62 ComandoMuitoLongo {
63 max: usize,
65 len: usize,
67 },
68
69 #[error("sudo/su desabilitado para este host (disable_sudo)")]
71 SudoDesabilitado,
72
73 #[error("senha_su não configurada; use vps edit --su-password ou --su-password-stdin")]
75 SenhaSuAusente,
76
77 #[error("canal SSH falhou: {0}")]
79 CanalFalhou(String),
80
81 #[error("timeout SSH após {0}ms")]
83 TimeoutSsh(u64),
84
85 #[error("comando falhou com exit code {exit_code}: {stderr}")]
87 ComandoFalhou {
88 exit_code: i32,
90 stderr: String,
92 },
93
94 #[error("VPS '{0}' não encontrada no registro")]
96 VpsNaoEncontrada(String),
97
98 #[error("Nenhuma VPS ativa. Use 'ssh-cli connect <NOME>' primeiro.")]
100 NenhumaVpsAtiva,
101
102 #[error("VPS '{0}' já existe no registro")]
104 VpsDuplicada(String),
105
106 #[error("arquivo não encontrado: {0}")]
108 ArquivoNaoEncontrado(String),
109
110 #[error("argumento inválido: {0}")]
112 ArgumentoInvalido(String),
113
114 #[error("timeout excedido após {0}ms")]
116 Timeout(u64),
117
118 #[error("diretório de configuração indisponível")]
120 DiretorioXdg,
121
122 #[error("versão de schema incompatível: esperada {esperada}, encontrada {encontrada}")]
124 SchemaIncompativel {
125 esperada: u32,
127 encontrada: u32,
129 },
130
131 #[error("erro: {0}")]
133 Generico(String),
134}
135
136pub mod exit_codes {
138 pub const EX_OK: i32 = 0;
140 pub const EX_GENERAL: i32 = 1;
142 pub const EX_USAGE: i32 = 64;
144 pub const EX_DATAERR: i32 = 65;
146 pub const EX_NOINPUT: i32 = 66;
148 pub const EX_CANTCREAT: i32 = 73;
150 pub const EX_IOERR: i32 = 74;
152 pub const EX_NOPERM: i32 = 77;
154 pub const EX_SIGINT: i32 = 130;
156 pub const EX_SIGTERM: i32 = 143;
158}
159
160impl ErroSshCli {
161 #[must_use]
163 pub fn exit_code(&self) -> i32 {
164 match self {
165 Self::Io(_) => exit_codes::EX_IOERR,
166 Self::Json(_) => exit_codes::EX_DATAERR,
167 Self::TomlDe(_) => exit_codes::EX_DATAERR,
168 Self::TomlSer(_) => exit_codes::EX_CANTCREAT,
169 Self::ConexaoSsh(_) => exit_codes::EX_IOERR,
170 Self::AutenticacaoSsh(_) => exit_codes::EX_IOERR,
171 Self::ConexaoFalhou(_) => exit_codes::EX_IOERR,
172 Self::AutenticacaoFalhou => exit_codes::EX_NOPERM,
173 Self::HostKeyMudou { .. } => exit_codes::EX_NOPERM,
174 Self::ComandoMuitoLongo { .. } => exit_codes::EX_USAGE,
175 Self::SudoDesabilitado => exit_codes::EX_NOPERM,
176 Self::SenhaSuAusente => exit_codes::EX_USAGE,
177 Self::CanalFalhou(_) => exit_codes::EX_IOERR,
178 Self::TimeoutSsh(_) => exit_codes::EX_IOERR,
179 Self::ComandoFalhou { .. } => exit_codes::EX_GENERAL,
182 Self::VpsNaoEncontrada(_) => exit_codes::EX_NOINPUT,
183 Self::NenhumaVpsAtiva => exit_codes::EX_NOINPUT,
184 Self::VpsDuplicada(_) => exit_codes::EX_USAGE,
185 Self::ArquivoNaoEncontrado(_) => exit_codes::EX_NOINPUT,
186 Self::ArgumentoInvalido(_) => exit_codes::EX_USAGE,
187 Self::Timeout(_) => exit_codes::EX_IOERR,
188 Self::DiretorioXdg => exit_codes::EX_CANTCREAT,
189 Self::SchemaIncompativel { .. } => exit_codes::EX_DATAERR,
190 Self::Generico(_) => exit_codes::EX_GENERAL,
191 }
192 }
193}
194
195pub type ResultadoSshCli<T> = std::result::Result<T, ErroSshCli>;
197
198#[cfg(test)]
199mod testes {
200 use super::*;
201
202 #[test]
203 fn vps_nao_encontrada_mensagem_contem_nome() {
204 let erro = ErroSshCli::VpsNaoEncontrada("producao".into());
205 assert!(erro.to_string().contains("producao"));
206 }
207
208 #[test]
209 fn vps_duplicada_mensagem_contem_nome() {
210 let erro = ErroSshCli::VpsDuplicada("vps-1".into());
211 let msg = erro.to_string();
212 assert!(msg.contains("vps-1"));
213 assert!(msg.contains("já existe"));
214 }
215
216 #[test]
217 fn erro_io_exibe_mensagem() {
218 let erro = ErroSshCli::from(std::io::Error::new(
219 std::io::ErrorKind::NotFound,
220 "arquivo nao encontrado",
221 ));
222 let msg = erro.to_string();
223 assert!(msg.contains("I/O") || msg.contains("arquivo nao encontrado"));
224 }
225
226 #[test]
227 fn erro_toml_de_exibe_mensagem() {
228 let toml_err = "invalid TOML".parse::<toml::Value>().unwrap_err();
229 let erro = ErroSshCli::TomlDe(toml_err);
230 let msg = erro.to_string();
231 assert!(msg.contains("TOML") || msg.contains("leitura"));
232 }
233
234 #[test]
235 fn erro_tipo_servidor_vps_nao_encontrada() {
236 let erro = ErroSshCli::VpsNaoEncontrada("servidor-x".into());
237 let msg = erro.to_string();
238 assert!(msg.contains("servidor-x"));
239 assert!(msg.contains("não encontrada") || msg.contains("not found"));
240 }
241
242 #[test]
243 fn exit_code_io_retorna_ioerr() {
244 let e = ErroSshCli::Io(std::io::Error::other("teste"));
245 assert_eq!(e.exit_code(), exit_codes::EX_IOERR);
246 }
247
248 #[test]
249 fn exit_code_autenticacao_falhou_retorna_noperm() {
250 assert_eq!(
251 ErroSshCli::AutenticacaoFalhou.exit_code(),
252 exit_codes::EX_NOPERM
253 );
254 }
255
256 #[test]
257 fn exit_code_vps_nao_encontrada_retorna_noinput() {
258 let e = ErroSshCli::VpsNaoEncontrada("teste".to_string());
259 assert_eq!(e.exit_code(), exit_codes::EX_NOINPUT);
260 }
261
262 #[test]
263 fn exit_code_nenhuma_vps_ativa_retorna_noinput() {
264 assert_eq!(
265 ErroSshCli::NenhumaVpsAtiva.exit_code(),
266 exit_codes::EX_NOINPUT
267 );
268 }
269
270 #[test]
271 fn exit_code_comando_falhou_usa_ex_general_nao_remoto() {
272 let e = ErroSshCli::ComandoFalhou {
273 exit_code: 127,
274 stderr: "not found".to_string(),
275 };
276 assert_eq!(e.exit_code(), exit_codes::EX_GENERAL);
277 let e64 = ErroSshCli::ComandoFalhou {
278 exit_code: 64,
279 stderr: "usage".to_string(),
280 };
281 assert_eq!(e64.exit_code(), exit_codes::EX_GENERAL);
282 }
283
284 #[test]
285 fn exit_code_argumento_invalido_retorna_usage() {
286 let e = ErroSshCli::ArgumentoInvalido("bad".to_string());
287 assert_eq!(e.exit_code(), exit_codes::EX_USAGE);
288 }
289
290 #[test]
291 fn exit_code_diretorio_xdg_retorna_cantcreat() {
292 assert_eq!(
293 ErroSshCli::DiretorioXdg.exit_code(),
294 exit_codes::EX_CANTCREAT
295 );
296 }
297}