1use std::backtrace::Backtrace;
19use std::fmt;
20
21#[derive(Debug)]
27pub(crate) enum GitwayErrorKind {
28 Io(std::io::Error),
30 Ssh(russh::Error),
32 Keys(russh::keys::Error),
34 HostKeyMismatch { fingerprint: String },
39 AuthenticationFailed,
41 NoKeyFound,
43 InvalidConfig { message: String },
45 Signing { message: String },
47 SignatureInvalid { reason: String },
49}
50
51impl fmt::Display for GitwayErrorKind {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 Self::Io(e) => write!(f, "I/O error: {e}"),
55 Self::Ssh(e) => write!(f, "SSH protocol error: {e}"),
56 Self::Keys(e) => write!(f, "SSH key error: {e}"),
57 Self::HostKeyMismatch { fingerprint } => {
58 write!(
59 f,
60 "host key mismatch — received fingerprint {fingerprint} \
61 does not match any pinned fingerprint"
62 )
63 }
64 Self::AuthenticationFailed => write!(f, "public-key authentication failed"),
65 Self::NoKeyFound => {
66 write!(f, "no SSH identity key found on any search path or agent")
67 }
68 Self::InvalidConfig { message } => write!(f, "invalid configuration: {message}"),
69 Self::Signing { message } => write!(f, "SSH signing failed: {message}"),
70 Self::SignatureInvalid { reason } => {
71 write!(f, "SSH signature verification failed: {reason}")
72 }
73 }
74 }
75}
76
77#[derive(Debug)]
96pub struct GitwayError {
97 kind: GitwayErrorKind,
98 backtrace: Backtrace,
99}
100
101impl GitwayError {
102 pub(crate) fn new(kind: GitwayErrorKind) -> Self {
104 Self {
105 kind,
106 backtrace: Backtrace::capture(),
107 }
108 }
109
110 pub fn host_key_mismatch(fingerprint: impl Into<String>) -> Self {
113 Self::new(GitwayErrorKind::HostKeyMismatch {
114 fingerprint: fingerprint.into(),
115 })
116 }
117
118 #[must_use]
119 pub fn authentication_failed() -> Self {
120 Self::new(GitwayErrorKind::AuthenticationFailed)
121 }
122
123 #[must_use]
124 pub fn no_key_found() -> Self {
125 Self::new(GitwayErrorKind::NoKeyFound)
126 }
127
128 pub fn invalid_config(message: impl Into<String>) -> Self {
129 Self::new(GitwayErrorKind::InvalidConfig {
130 message: message.into(),
131 })
132 }
133
134 pub fn signing(message: impl Into<String>) -> Self {
138 Self::new(GitwayErrorKind::Signing {
139 message: message.into(),
140 })
141 }
142
143 pub fn signature_invalid(reason: impl Into<String>) -> Self {
148 Self::new(GitwayErrorKind::SignatureInvalid {
149 reason: reason.into(),
150 })
151 }
152
153 #[must_use]
157 pub fn is_io(&self) -> bool {
158 matches!(self.kind, GitwayErrorKind::Io(_))
159 }
160
161 #[must_use]
163 pub fn is_host_key_mismatch(&self) -> bool {
164 matches!(self.kind, GitwayErrorKind::HostKeyMismatch { .. })
165 }
166
167 #[must_use]
169 pub fn is_authentication_failed(&self) -> bool {
170 matches!(self.kind, GitwayErrorKind::AuthenticationFailed)
171 }
172
173 #[must_use]
175 pub fn is_no_key_found(&self) -> bool {
176 matches!(self.kind, GitwayErrorKind::NoKeyFound)
177 }
178
179 #[must_use]
181 pub fn is_key_encrypted(&self) -> bool {
182 matches!(
183 self.kind,
184 GitwayErrorKind::Keys(russh::keys::Error::KeyIsEncrypted)
185 )
186 }
187
188 #[must_use]
190 pub fn fingerprint(&self) -> Option<&str> {
191 match &self.kind {
192 GitwayErrorKind::HostKeyMismatch { fingerprint } => Some(fingerprint),
193 _ => None,
194 }
195 }
196
197 #[must_use]
206 pub fn error_code(&self) -> &'static str {
207 match &self.kind {
208 GitwayErrorKind::InvalidConfig { .. } => "USAGE_ERROR",
209 GitwayErrorKind::NoKeyFound => "NOT_FOUND",
210 GitwayErrorKind::HostKeyMismatch { .. }
211 | GitwayErrorKind::AuthenticationFailed
212 | GitwayErrorKind::SignatureInvalid { .. } => "PERMISSION_DENIED",
213 GitwayErrorKind::Io(_)
214 | GitwayErrorKind::Ssh(_)
215 | GitwayErrorKind::Keys(_)
216 | GitwayErrorKind::Signing { .. } => "GENERAL_ERROR",
217 }
218 }
219
220 #[must_use]
229 pub fn exit_code(&self) -> u32 {
230 match &self.kind {
231 GitwayErrorKind::InvalidConfig { .. } => 2,
232 GitwayErrorKind::NoKeyFound => 3,
233 GitwayErrorKind::HostKeyMismatch { .. }
234 | GitwayErrorKind::AuthenticationFailed
235 | GitwayErrorKind::SignatureInvalid { .. } => 4,
236 GitwayErrorKind::Io(_)
237 | GitwayErrorKind::Ssh(_)
238 | GitwayErrorKind::Keys(_)
239 | GitwayErrorKind::Signing { .. } => 1,
240 }
241 }
242
243 #[must_use]
245 pub fn hint(&self) -> &'static str {
246 match &self.kind {
247 GitwayErrorKind::HostKeyMismatch { .. } => {
248 "Run 'gitway --test --verbose' to diagnose, \
249 or check ~/.config/gitway/known_hosts"
250 }
251 GitwayErrorKind::AuthenticationFailed => {
252 "Ensure your SSH public key is registered with the Git hosting service, \
253 or run 'ssh-add' to load a key into the agent"
254 }
255 GitwayErrorKind::NoKeyFound => {
256 "Run 'ssh-keygen -t ed25519' to generate a key, or use --identity to specify one"
257 }
258 GitwayErrorKind::InvalidConfig { .. } => "Run 'gitway --help' for usage information",
259 GitwayErrorKind::Signing { .. } => {
260 "Ensure the private key is readable and the passphrase is correct; \
261 run with --verbose to see the underlying cryptographic error"
262 }
263 GitwayErrorKind::SignatureInvalid { .. } => {
264 "The signature is invalid, was signed with a different key, \
265 or uses a different namespace than expected"
266 }
267 GitwayErrorKind::Io(_) | GitwayErrorKind::Ssh(_) | GitwayErrorKind::Keys(_) => {
268 "Run 'gitway --test --verbose' to diagnose the connection"
269 }
270 }
271 }
272}
273
274impl fmt::Display for GitwayError {
277 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278 write!(f, "{}", self.kind)?;
279 let bt = self.backtrace.to_string();
280 if !bt.is_empty() && bt != "disabled backtrace" {
281 write!(f, "\n\nstack backtrace:\n{bt}")?;
282 }
283 Ok(())
284 }
285}
286
287impl std::error::Error for GitwayError {
288 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
289 match &self.kind {
290 GitwayErrorKind::Io(e) => Some(e),
291 GitwayErrorKind::Ssh(e) => Some(e),
292 GitwayErrorKind::Keys(e) => Some(e),
293 _ => None,
294 }
295 }
296}
297
298impl From<russh::Error> for GitwayError {
299 fn from(e: russh::Error) -> Self {
300 Self::new(GitwayErrorKind::Ssh(e))
301 }
302}
303
304impl From<russh::keys::Error> for GitwayError {
305 fn from(e: russh::keys::Error) -> Self {
306 Self::new(GitwayErrorKind::Keys(e))
307 }
308}
309
310impl From<std::io::Error> for GitwayError {
311 fn from(e: std::io::Error) -> Self {
312 Self::new(GitwayErrorKind::Io(e))
313 }
314}
315
316impl From<russh::AgentAuthError> for GitwayError {
317 fn from(e: russh::AgentAuthError) -> Self {
318 match e {
319 russh::AgentAuthError::Send(_) => {
320 Self::new(GitwayErrorKind::Ssh(russh::Error::SendError))
321 }
322 russh::AgentAuthError::Key(k) => Self::new(GitwayErrorKind::Keys(k)),
323 }
324 }
325}