Skip to main content

ssh_cli/
erros.rs

1//! Tipos de erro do ssh-cli.
2//!
3//! Define o enum [`ErroSshCli`] com todas as categorias de erro do domínio
4//! usadas pela CLI.
5
6use thiserror::Error;
7
8/// Enum com todos os erros possíveis do ssh-cli.
9#[derive(Debug, Error)]
10pub enum ErroSshCli {
11    /// Erro de I/O subjacente.
12    #[error("erro de I/O: {0}")]
13    Io(#[from] std::io::Error),
14
15    /// Erro de serialização/deserialização JSON.
16    #[error("erro de JSON: {0}")]
17    Json(#[from] serde_json::Error),
18
19    /// Erro de deserialização TOML.
20    #[error("erro de TOML (leitura): {0}")]
21    TomlDe(#[from] toml::de::Error),
22
23    /// Erro de serialização TOML.
24    #[error("erro de TOML (escrita): {0}")]
25    TomlSer(#[from] toml::ser::Error),
26
27    /// Erro de conexão SSH.
28    #[error("erro de conexão SSH: {0}")]
29    ConexaoSsh(String),
30
31    /// Erro de autenticação SSH.
32    #[error("erro de autenticação SSH: {0}")]
33    AutenticacaoSsh(String),
34
35    /// Falha ao estabelecer conexão TCP/SSH (passo anterior à autenticação).
36    #[error("conexão SSH falhou: {0}")]
37    ConexaoFalhou(String),
38
39    /// Autenticação SSH rejeitada pelo servidor.
40    #[error(
41        "autenticação SSH falhou; tente --password-stdin, --key PATH, --key-passphrase-stdin ou verifique o usuário"
42    )]
43    AutenticacaoFalhou,
44
45    /// Host key divergiu do known_hosts (possível MITM).
46    #[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.
51        host: String,
52        /// Porta.
53        porta: u16,
54        /// Fingerprint esperado.
55        esperado: String,
56        /// Fingerprint obtido.
57        obtido: String,
58    },
59
60    /// Comando excede `max_command_chars`.
61    #[error("comando excede max_command_chars ({max}): {len} caracteres")]
62    ComandoMuitoLongo {
63        /// Limite configurado.
64        max: usize,
65        /// Tamanho do comando.
66        len: usize,
67    },
68
69    /// Elevação desabilitada (`disable_sudo`).
70    #[error("sudo/su desabilitado para este host (disable_sudo)")]
71    SudoDesabilitado,
72
73    /// Senha su ausente para `su-exec`.
74    #[error("senha_su não configurada; use vps edit --su-password ou --su-password-stdin")]
75    SenhaSuAusente,
76
77    /// Falha ao abrir ou operar um canal SSH.
78    #[error("canal SSH falhou: {0}")]
79    CanalFalhou(String),
80
81    /// Timeout específico em operação SSH.
82    #[error("timeout SSH após {0}ms")]
83    TimeoutSsh(u64),
84
85    /// Comando remoto terminou com código de saída diferente de zero.
86    #[error("comando falhou com exit code {exit_code}: {stderr}")]
87    ComandoFalhou {
88        /// Código de saída retornado pelo comando remoto.
89        exit_code: i32,
90        /// Trecho (possivelmente truncado) de stderr.
91        stderr: String,
92    },
93
94    /// VPS não encontrada no registro.
95    #[error("VPS '{0}' não encontrada no registro")]
96    VpsNaoEncontrada(String),
97
98    /// VPS com nome duplicado no registro.
99    #[error("VPS '{0}' já existe no registro")]
100    VpsDuplicada(String),
101
102    /// Arquivo local não encontrado.
103    #[error("arquivo não encontrado: {0}")]
104    ArquivoNaoEncontrado(String),
105
106    /// Argumento inválido recebido via CLI.
107    #[error("argumento inválido: {0}")]
108    ArgumentoInvalido(String),
109
110    /// Timeout excedido em operação.
111    #[error("timeout excedido após {0}ms")]
112    Timeout(u64),
113
114    /// Erro de diretório XDG.
115    #[error("diretório de configuração indisponível")]
116    DiretorioXdg,
117
118    /// Versão de schema incompatível.
119    #[error("versão de schema incompatível: esperada {esperada}, encontrada {encontrada}")]
120    SchemaIncompativel {
121        /// Versão esperada.
122        esperada: u32,
123        /// Versão encontrada no arquivo.
124        encontrada: u32,
125    },
126
127    /// Erro genérico não categorizado.
128    #[error("erro: {0}")]
129    Generico(String),
130}
131
132/// Exit codes padronizados conforme sysexits.h e convenções de sinais Unix.
133pub mod exit_codes {
134    /// Sucesso.
135    pub const EX_OK: i32 = 0;
136    /// Erro genérico de domínio.
137    pub const EX_GENERAL: i32 = 1;
138    /// Uso incorreto da CLI (argumento inválido).
139    pub const EX_USAGE: i32 = 64;
140    /// Dados de entrada inválidos.
141    pub const EX_DATAERR: i32 = 65;
142    /// Entrada não encontrada.
143    pub const EX_NOINPUT: i32 = 66;
144    /// Não foi possível criar saída.
145    pub const EX_CANTCREAT: i32 = 73;
146    /// Erro de I/O.
147    pub const EX_IOERR: i32 = 74;
148    /// Permissão negada.
149    pub const EX_NOPERM: i32 = 77;
150    /// Terminado por SIGINT (Ctrl+C).
151    pub const EX_SIGINT: i32 = 130;
152    /// Terminado por SIGTERM.
153    pub const EX_SIGTERM: i32 = 143;
154}
155
156impl ErroSshCli {
157    /// Retorna o exit code sysexits.h correspondente a este erro.
158    #[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
188/// Alias de `Result` usando o tipo de erro do ssh-cli.
189pub 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}