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("VPS '{0}' já existe no registro")]
100 VpsDuplicada(String),
101
102 #[error("arquivo não encontrado: {0}")]
104 ArquivoNaoEncontrado(String),
105
106 #[error("argumento inválido: {0}")]
108 ArgumentoInvalido(String),
109
110 #[error("timeout excedido após {0}ms")]
112 Timeout(u64),
113
114 #[error("diretório de configuração indisponível")]
116 DiretorioXdg,
117
118 #[error("versão de schema incompatível: esperada {esperada}, encontrada {encontrada}")]
120 SchemaIncompativel {
121 esperada: u32,
123 encontrada: u32,
125 },
126
127 #[error("erro: {0}")]
129 Generico(String),
130}
131
132pub mod exit_codes {
134 pub const EX_OK: i32 = 0;
136 pub const EX_GENERAL: i32 = 1;
138 pub const EX_USAGE: i32 = 64;
140 pub const EX_DATAERR: i32 = 65;
142 pub const EX_NOINPUT: i32 = 66;
144 pub const EX_CANTCREAT: i32 = 73;
146 pub const EX_IOERR: i32 = 74;
148 pub const EX_NOPERM: i32 = 77;
150 pub const EX_SIGINT: i32 = 130;
152 pub const EX_SIGTERM: i32 = 143;
154}
155
156impl ErroSshCli {
157 #[must_use]
159 pub fn exit_code(&self) -> i32 {
160 match self {
161 Self::Io(_) => exit_codes::EX_IOERR,
162 Self::Json(_) => exit_codes::EX_DATAERR,
163 Self::TomlDe(_) => exit_codes::EX_DATAERR,
164 Self::TomlSer(_) => exit_codes::EX_CANTCREAT,
165 Self::ConexaoSsh(_) => exit_codes::EX_IOERR,
166 Self::AutenticacaoSsh(_) => exit_codes::EX_IOERR,
167 Self::ConexaoFalhou(_) => exit_codes::EX_IOERR,
168 Self::AutenticacaoFalhou => exit_codes::EX_NOPERM,
169 Self::HostKeyMudou { .. } => exit_codes::EX_NOPERM,
170 Self::ComandoMuitoLongo { .. } => exit_codes::EX_USAGE,
171 Self::SudoDesabilitado => exit_codes::EX_NOPERM,
172 Self::SenhaSuAusente => exit_codes::EX_USAGE,
173 Self::CanalFalhou(_) => exit_codes::EX_IOERR,
174 Self::TimeoutSsh(_) => exit_codes::EX_IOERR,
175 Self::ComandoFalhou { exit_code, .. } => *exit_code,
176 Self::VpsNaoEncontrada(_) => exit_codes::EX_NOINPUT,
177 Self::VpsDuplicada(_) => exit_codes::EX_USAGE,
178 Self::ArquivoNaoEncontrado(_) => exit_codes::EX_NOINPUT,
179 Self::ArgumentoInvalido(_) => exit_codes::EX_USAGE,
180 Self::Timeout(_) => exit_codes::EX_IOERR,
181 Self::DiretorioXdg => exit_codes::EX_CANTCREAT,
182 Self::SchemaIncompativel { .. } => exit_codes::EX_DATAERR,
183 Self::Generico(_) => exit_codes::EX_GENERAL,
184 }
185 }
186}
187
188pub type ResultadoSshCli<T> = std::result::Result<T, ErroSshCli>;
190
191#[cfg(test)]
192mod testes {
193 use super::*;
194
195 #[test]
196 fn vps_nao_encontrada_mensagem_contem_nome() {
197 let erro = ErroSshCli::VpsNaoEncontrada("producao".into());
198 assert!(erro.to_string().contains("producao"));
199 }
200
201 #[test]
202 fn vps_duplicada_mensagem_contem_nome() {
203 let erro = ErroSshCli::VpsDuplicada("vps-1".into());
204 let msg = erro.to_string();
205 assert!(msg.contains("vps-1"));
206 assert!(msg.contains("já existe"));
207 }
208
209 #[test]
210 fn erro_io_exibe_mensagem() {
211 let erro = ErroSshCli::from(std::io::Error::new(
212 std::io::ErrorKind::NotFound,
213 "arquivo nao encontrado",
214 ));
215 let msg = erro.to_string();
216 assert!(msg.contains("I/O") || msg.contains("arquivo nao encontrado"));
217 }
218
219 #[test]
220 fn erro_toml_de_exibe_mensagem() {
221 let toml_err = "invalid TOML".parse::<toml::Value>().unwrap_err();
222 let erro = ErroSshCli::TomlDe(toml_err);
223 let msg = erro.to_string();
224 assert!(msg.contains("TOML") || msg.contains("leitura"));
225 }
226
227 #[test]
228 fn erro_tipo_servidor_vps_nao_encontrada() {
229 let erro = ErroSshCli::VpsNaoEncontrada("servidor-x".into());
230 let msg = erro.to_string();
231 assert!(msg.contains("servidor-x"));
232 assert!(msg.contains("não encontrada") || msg.contains("not found"));
233 }
234
235 #[test]
236 fn exit_code_io_retorna_ioerr() {
237 let e = ErroSshCli::Io(std::io::Error::other("teste"));
238 assert_eq!(e.exit_code(), exit_codes::EX_IOERR);
239 }
240
241 #[test]
242 fn exit_code_autenticacao_falhou_retorna_noperm() {
243 assert_eq!(
244 ErroSshCli::AutenticacaoFalhou.exit_code(),
245 exit_codes::EX_NOPERM
246 );
247 }
248
249 #[test]
250 fn exit_code_vps_nao_encontrada_retorna_noinput() {
251 let e = ErroSshCli::VpsNaoEncontrada("teste".to_string());
252 assert_eq!(e.exit_code(), exit_codes::EX_NOINPUT);
253 }
254
255 #[test]
256 fn exit_code_comando_falhou_propaga_exit_code_remoto() {
257 let e = ErroSshCli::ComandoFalhou {
258 exit_code: 127,
259 stderr: "not found".to_string(),
260 };
261 assert_eq!(e.exit_code(), 127);
262 }
263
264 #[test]
265 fn exit_code_argumento_invalido_retorna_usage() {
266 let e = ErroSshCli::ArgumentoInvalido("bad".to_string());
267 assert_eq!(e.exit_code(), exit_codes::EX_USAGE);
268 }
269
270 #[test]
271 fn exit_code_diretorio_xdg_retorna_cantcreat() {
272 assert_eq!(
273 ErroSshCli::DiretorioXdg.exit_code(),
274 exit_codes::EX_CANTCREAT
275 );
276 }
277}