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 a mensagem do erro traduzida via i18n com base no idioma global.
124    ///
125    /// Mapeia variantes do erro para a enum [`crate::i18n::Mensagem`] quando
126    /// há tradução disponível e retorna o texto no idioma atual. Variantes
127    /// sem mapeamento i18n caem no `Display` padrão gerado por `thiserror`.
128    ///
129    /// Essa função é o ÚNICO ponto autorizado a converter erros do domínio
130    /// em texto de UI, garantindo que a camada [`crate::i18n`] seja consultada
131    /// no path de erro e a precedência `--lang` > `SSH_CLI_LANG` > locale seja
132    /// respeitada na saída final ao usuário.
133    #[must_use]
134    pub fn mensagem_i18n(&self) -> String {
135        use crate::i18n::{t, Mensagem};
136        match self {
137            Self::VpsNaoEncontrada(nome) => t(Mensagem::VpsNaoEncontrada { nome: nome.clone() }),
138            Self::VpsDuplicada(nome) => t(Mensagem::VpsDuplicada { nome: nome.clone() }),
139            Self::ArgumentoInvalido(detalhe) => t(Mensagem::ErroArgumentoInvalido {
140                detalhe: detalhe.clone(),
141            }),
142            Self::ConexaoSsh(detalhe)
143            | Self::AutenticacaoSsh(detalhe)
144            | Self::ConexaoFalhou(detalhe)
145            | Self::CanalFalhou(detalhe) => t(Mensagem::ErroGenerico {
146                detalhe: format!("{}: {detalhe}", t(Mensagem::ErroConexaoSsh)),
147            }),
148            Self::AutenticacaoFalhou => t(Mensagem::ErroConexaoSsh),
149            Self::ComandoFalhou { exit_code, stderr } => t(Mensagem::ErroGenerico {
150                detalhe: format!(
151                    "{} (exit={exit_code}): {stderr}",
152                    t(Mensagem::ErroComandoFalhou)
153                ),
154            }),
155            Self::TimeoutSsh(ms) | Self::Timeout(ms) => t(Mensagem::ErroGenerico {
156                detalhe: format!("timeout: {ms}ms"),
157            }),
158            // Variantes sem mapeamento específico: recorre ao Display padrão
159            // (preserva comportamento para tipos de erro técnicos como Io/Json/Toml).
160            _ => self.to_string(),
161        }
162    }
163
164    /// Retorna o exit code sysexits.h correspondente a este erro.
165    #[must_use]
166    pub fn exit_code(&self) -> i32 {
167        match self {
168            Self::Io(_) => exit_codes::EX_IOERR,
169            Self::Json(_) => exit_codes::EX_DATAERR,
170            Self::TomlDe(_) => exit_codes::EX_DATAERR,
171            Self::TomlSer(_) => exit_codes::EX_CANTCREAT,
172            Self::ConexaoSsh(_) => exit_codes::EX_IOERR,
173            Self::AutenticacaoSsh(_) => exit_codes::EX_IOERR,
174            Self::ConexaoFalhou(_) => exit_codes::EX_IOERR,
175            Self::AutenticacaoFalhou => exit_codes::EX_NOPERM,
176            Self::CanalFalhou(_) => exit_codes::EX_IOERR,
177            Self::TimeoutSsh(_) => exit_codes::EX_IOERR,
178            Self::ComandoFalhou { exit_code, .. } => *exit_code,
179            Self::VpsNaoEncontrada(_) => exit_codes::EX_NOINPUT,
180            Self::VpsDuplicada(_) => exit_codes::EX_USAGE,
181            Self::ArquivoNaoEncontrado(_) => exit_codes::EX_NOINPUT,
182            Self::ArgumentoInvalido(_) => exit_codes::EX_USAGE,
183            Self::Timeout(_) => exit_codes::EX_IOERR,
184            Self::DiretorioXdg => exit_codes::EX_CANTCREAT,
185            Self::SchemaIncompativel { .. } => exit_codes::EX_DATAERR,
186            Self::Generico(_) => exit_codes::EX_GENERAL,
187        }
188    }
189}
190
191/// Alias de `Result` usando o tipo de erro do ssh-cli.
192pub type ResultadoSshCli<T> = std::result::Result<T, ErroSshCli>;
193
194#[cfg(test)]
195mod testes {
196    use super::*;
197
198    #[test]
199    fn vps_nao_encontrada_mensagem_contem_nome() {
200        let erro = ErroSshCli::VpsNaoEncontrada("producao".into());
201        assert!(erro.to_string().contains("producao"));
202    }
203
204    #[test]
205    fn vps_duplicada_mensagem_contem_nome() {
206        let erro = ErroSshCli::VpsDuplicada("vps-1".into());
207        let msg = erro.to_string();
208        assert!(msg.contains("vps-1"));
209        assert!(msg.contains("já existe"));
210    }
211
212    #[test]
213    fn erro_io_exibe_mensagem() {
214        let erro = ErroSshCli::from(std::io::Error::new(
215            std::io::ErrorKind::NotFound,
216            "arquivo nao encontrado",
217        ));
218        let msg = erro.to_string();
219        assert!(msg.contains("I/O") || msg.contains("arquivo nao encontrado"));
220    }
221
222    #[test]
223    fn erro_toml_de_exibe_mensagem() {
224        let toml_err = "invalid TOML".parse::<toml::Value>().unwrap_err();
225        let erro = ErroSshCli::TomlDe(toml_err);
226        let msg = erro.to_string();
227        assert!(msg.contains("TOML") || msg.contains("leitura"));
228    }
229
230    #[test]
231    fn erro_tipo_servidor_vps_nao_encontrada() {
232        let erro = ErroSshCli::VpsNaoEncontrada("servidor-x".into());
233        let msg = erro.to_string();
234        assert!(msg.contains("servidor-x"));
235        assert!(msg.contains("não encontrada") || msg.contains("not found"));
236    }
237
238    #[test]
239    fn exit_code_io_retorna_ioerr() {
240        let e = ErroSshCli::Io(std::io::Error::other("teste"));
241        assert_eq!(e.exit_code(), exit_codes::EX_IOERR);
242    }
243
244    #[test]
245    fn exit_code_autenticacao_falhou_retorna_noperm() {
246        assert_eq!(
247            ErroSshCli::AutenticacaoFalhou.exit_code(),
248            exit_codes::EX_NOPERM
249        );
250    }
251
252    #[test]
253    fn exit_code_vps_nao_encontrada_retorna_noinput() {
254        let e = ErroSshCli::VpsNaoEncontrada("teste".to_string());
255        assert_eq!(e.exit_code(), exit_codes::EX_NOINPUT);
256    }
257
258    #[test]
259    fn exit_code_comando_falhou_propaga_exit_code_remoto() {
260        let e = ErroSshCli::ComandoFalhou {
261            exit_code: 127,
262            stderr: "not found".to_string(),
263        };
264        assert_eq!(e.exit_code(), 127);
265    }
266
267    #[test]
268    fn exit_code_argumento_invalido_retorna_usage() {
269        let e = ErroSshCli::ArgumentoInvalido("bad".to_string());
270        assert_eq!(e.exit_code(), exit_codes::EX_USAGE);
271    }
272
273    #[test]
274    fn exit_code_diretorio_xdg_retorna_cantcreat() {
275        assert_eq!(
276            ErroSshCli::DiretorioXdg.exit_code(),
277            exit_codes::EX_CANTCREAT
278        );
279    }
280}