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("autenticação SSH falhou")]
41    AutenticacaoFalhou,
42
43    /// Falha ao abrir ou operar um canal SSH.
44    #[error("canal SSH falhou: {0}")]
45    CanalFalhou(String),
46
47    /// Timeout específico em operação SSH.
48    #[error("timeout SSH após {0}ms")]
49    TimeoutSsh(u64),
50
51    /// Comando remoto terminou com código de saída diferente de zero.
52    #[error("comando falhou com exit code {exit_code}: {stderr}")]
53    ComandoFalhou {
54        /// Código de saída retornado pelo comando remoto.
55        exit_code: i32,
56        /// Trecho (possivelmente truncado) de stderr.
57        stderr: String,
58    },
59
60    /// VPS não encontrada no registro.
61    #[error("VPS '{0}' não encontrada no registro")]
62    VpsNaoEncontrada(String),
63
64    /// VPS com nome duplicado no registro.
65    #[error("VPS '{0}' já existe no registro")]
66    VpsDuplicada(String),
67
68    /// Arquivo local não encontrado.
69    #[error("arquivo não encontrado: {0}")]
70    ArquivoNaoEncontrado(String),
71
72    /// Argumento inválido recebido via CLI.
73    #[error("argumento inválido: {0}")]
74    ArgumentoInvalido(String),
75
76    /// Timeout excedido em operação.
77    #[error("timeout excedido após {0}ms")]
78    Timeout(u64),
79
80    /// Erro de diretório XDG.
81    #[error("diretório de configuração indisponível")]
82    DiretorioXdg,
83
84    /// Versão de schema incompatível.
85    #[error("versão de schema incompatível: esperada {esperada}, encontrada {encontrada}")]
86    SchemaIncompativel {
87        /// Versão esperada.
88        esperada: u32,
89        /// Versão encontrada no arquivo.
90        encontrada: u32,
91    },
92
93    /// Erro genérico não categorizado.
94    #[error("erro: {0}")]
95    Generico(String),
96}
97
98/// Exit codes padronizados conforme sysexits.h e convenções de sinais Unix.
99pub mod exit_codes {
100    /// Sucesso.
101    pub const EX_OK: i32 = 0;
102    /// Erro genérico de domínio.
103    pub const EX_GENERAL: i32 = 1;
104    /// Uso incorreto da CLI (argumento inválido).
105    pub const EX_USAGE: i32 = 64;
106    /// Dados de entrada inválidos.
107    pub const EX_DATAERR: i32 = 65;
108    /// Entrada não encontrada.
109    pub const EX_NOINPUT: i32 = 66;
110    /// Não foi possível criar saída.
111    pub const EX_CANTCREAT: i32 = 73;
112    /// Erro de I/O.
113    pub const EX_IOERR: i32 = 74;
114    /// Permissão negada.
115    pub const EX_NOPERM: i32 = 77;
116    /// Terminado por SIGINT (Ctrl+C).
117    pub const EX_SIGINT: i32 = 130;
118    /// Terminado por SIGTERM.
119    pub const EX_SIGTERM: i32 = 143;
120}
121
122impl ErroSshCli {
123    /// Retorna o exit code sysexits.h correspondente a este erro.
124    #[must_use]
125    pub fn exit_code(&self) -> i32 {
126        match self {
127            Self::Io(_) => exit_codes::EX_IOERR,
128            Self::Json(_) => exit_codes::EX_DATAERR,
129            Self::TomlDe(_) => exit_codes::EX_DATAERR,
130            Self::TomlSer(_) => exit_codes::EX_CANTCREAT,
131            Self::ConexaoSsh(_) => exit_codes::EX_IOERR,
132            Self::AutenticacaoSsh(_) => exit_codes::EX_IOERR,
133            Self::ConexaoFalhou(_) => exit_codes::EX_IOERR,
134            Self::AutenticacaoFalhou => exit_codes::EX_NOPERM,
135            Self::CanalFalhou(_) => exit_codes::EX_IOERR,
136            Self::TimeoutSsh(_) => exit_codes::EX_IOERR,
137            Self::ComandoFalhou { exit_code, .. } => *exit_code,
138            Self::VpsNaoEncontrada(_) => exit_codes::EX_NOINPUT,
139            Self::VpsDuplicada(_) => exit_codes::EX_USAGE,
140            Self::ArquivoNaoEncontrado(_) => exit_codes::EX_NOINPUT,
141            Self::ArgumentoInvalido(_) => exit_codes::EX_USAGE,
142            Self::Timeout(_) => exit_codes::EX_IOERR,
143            Self::DiretorioXdg => exit_codes::EX_CANTCREAT,
144            Self::SchemaIncompativel { .. } => exit_codes::EX_DATAERR,
145            Self::Generico(_) => exit_codes::EX_GENERAL,
146        }
147    }
148}
149
150/// Alias de `Result` usando o tipo de erro do ssh-cli.
151pub type ResultadoSshCli<T> = std::result::Result<T, ErroSshCli>;
152
153#[cfg(test)]
154mod testes {
155    use super::*;
156
157    #[test]
158    fn vps_nao_encontrada_mensagem_contem_nome() {
159        let erro = ErroSshCli::VpsNaoEncontrada("producao".into());
160        assert!(erro.to_string().contains("producao"));
161    }
162
163    #[test]
164    fn vps_duplicada_mensagem_contem_nome() {
165        let erro = ErroSshCli::VpsDuplicada("vps-1".into());
166        let msg = erro.to_string();
167        assert!(msg.contains("vps-1"));
168        assert!(msg.contains("já existe"));
169    }
170
171    #[test]
172    fn erro_io_exibe_mensagem() {
173        let erro = ErroSshCli::from(std::io::Error::new(
174            std::io::ErrorKind::NotFound,
175            "arquivo nao encontrado",
176        ));
177        let msg = erro.to_string();
178        assert!(msg.contains("I/O") || msg.contains("arquivo nao encontrado"));
179    }
180
181    #[test]
182    fn erro_toml_de_exibe_mensagem() {
183        let toml_err = "invalid TOML".parse::<toml::Value>().unwrap_err();
184        let erro = ErroSshCli::TomlDe(toml_err);
185        let msg = erro.to_string();
186        assert!(msg.contains("TOML") || msg.contains("leitura"));
187    }
188
189    #[test]
190    fn erro_tipo_servidor_vps_nao_encontrada() {
191        let erro = ErroSshCli::VpsNaoEncontrada("servidor-x".into());
192        let msg = erro.to_string();
193        assert!(msg.contains("servidor-x"));
194        assert!(msg.contains("não encontrada") || msg.contains("not found"));
195    }
196
197    #[test]
198    fn exit_code_io_retorna_ioerr() {
199        let e = ErroSshCli::Io(std::io::Error::other("teste"));
200        assert_eq!(e.exit_code(), exit_codes::EX_IOERR);
201    }
202
203    #[test]
204    fn exit_code_autenticacao_falhou_retorna_noperm() {
205        assert_eq!(
206            ErroSshCli::AutenticacaoFalhou.exit_code(),
207            exit_codes::EX_NOPERM
208        );
209    }
210
211    #[test]
212    fn exit_code_vps_nao_encontrada_retorna_noinput() {
213        let e = ErroSshCli::VpsNaoEncontrada("teste".to_string());
214        assert_eq!(e.exit_code(), exit_codes::EX_NOINPUT);
215    }
216
217    #[test]
218    fn exit_code_comando_falhou_propaga_exit_code_remoto() {
219        let e = ErroSshCli::ComandoFalhou {
220            exit_code: 127,
221            stderr: "not found".to_string(),
222        };
223        assert_eq!(e.exit_code(), 127);
224    }
225
226    #[test]
227    fn exit_code_argumento_invalido_retorna_usage() {
228        let e = ErroSshCli::ArgumentoInvalido("bad".to_string());
229        assert_eq!(e.exit_code(), exit_codes::EX_USAGE);
230    }
231
232    #[test]
233    fn exit_code_diretorio_xdg_retorna_cantcreat() {
234        assert_eq!(
235            ErroSshCli::DiretorioXdg.exit_code(),
236            exit_codes::EX_CANTCREAT
237        );
238    }
239}