diff --git a/Cargo.lock b/Cargo.lock index b7ca2f8744b..2a35870c8b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2482,7 +2482,6 @@ dependencies = [ "pin-project-lite", "reqwest", "serde", - "thiserror 2.0.17", ] [[package]] diff --git a/gix-transport/Cargo.toml b/gix-transport/Cargo.toml index bfe32780494..0e59abb193e 100644 --- a/gix-transport/Cargo.toml +++ b/gix-transport/Cargo.toml @@ -95,7 +95,6 @@ bstr = { version = "1.12.0", default-features = false, features = [ "std", "unicode", ] } -thiserror = "2.0.17" # for async-client async-trait = { version = "0.1.51", optional = true } diff --git a/gix-transport/src/client/blocking_io/file.rs b/gix-transport/src/client/blocking_io/file.rs index b91f1c1a281..690c3876aeb 100644 --- a/gix-transport/src/client/blocking_io/file.rs +++ b/gix-transport/src/client/blocking_io/file.rs @@ -210,8 +210,7 @@ impl client::blocking_io::Transport for SpawnProcessOnDemand { ) -> Result, client::Error> { let (mut cmd, ssh_kind, cmd_name) = match &self.ssh_cmd { Some((command, kind)) => ( - kind.prepare_invocation(command, &self.url, self.desired_version, self.ssh_disallow_shell) - .map_err(client::Error::SshInvocation)? + kind.prepare_invocation(command, &self.url, self.desired_version, self.ssh_disallow_shell)? .stderr(Stdio::piped()), Some(*kind), Cow::Owned(command.to_owned()), diff --git a/gix-transport/src/client/blocking_io/http/curl/mod.rs b/gix-transport/src/client/blocking_io/http/curl/mod.rs index 99411fe20e7..fc1f659d7fc 100644 --- a/gix-transport/src/client/blocking_io/http/curl/mod.rs +++ b/gix-transport/src/client/blocking_io/http/curl/mod.rs @@ -21,27 +21,7 @@ pub struct Options { /// The error returned by the 'remote' helper, a purely internal construct to perform http requests. /// /// It can be used for downcasting errors, which are boxed to hide the actual implementation. -#[derive(Debug, thiserror::Error)] -#[allow(missing_docs)] -pub enum Error { - #[error(transparent)] - Curl(#[from] curl::Error), - #[error(transparent)] - Redirect(#[from] http::redirect::Error), - #[error("Could not finish reading all data to post to the remote")] - ReadPostBody(#[from] std::io::Error), - #[error(transparent)] - Authenticate(#[from] gix_credentials::protocol::Error), -} - -impl crate::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { - match self { - Error::Curl(err) => curl_is_spurious(err), - _ => false, - } - } -} +pub type Error = crate::Error; pub(crate) fn curl_is_spurious(err: &curl::Error) -> bool { err.is_couldnt_connect() @@ -77,7 +57,7 @@ impl Curl { self.handle = Some(handle); self.req = req; self.res = res; - err_that_brought_thread_down.into() + err_that_brought_thread_down } fn make_request( diff --git a/gix-transport/src/client/blocking_io/http/curl/remote.rs b/gix-transport/src/client/blocking_io/http/curl/remote.rs index c6b918441c2..4784e2522c9 100644 --- a/gix-transport/src/client/blocking_io/http/curl/remote.rs +++ b/gix-transport/src/client/blocking_io/http/curl/remote.rs @@ -378,19 +378,3 @@ fn to_curl_ssl_version(vers: SslVersion) -> curl::easy::SslVersion { SslVersion::TlsV1_3 => Tlsv13, } } - -impl From for http::Error { - fn from(err: Error) -> Self { - http::Error::Detail { - description: err.to_string(), - } - } -} - -impl From for http::Error { - fn from(err: curl::Error) -> Self { - http::Error::Detail { - description: err.to_string(), - } - } -} diff --git a/gix-transport/src/client/blocking_io/http/mod.rs b/gix-transport/src/client/blocking_io/http/mod.rs index df4bda43512..de16085322f 100644 --- a/gix-transport/src/client/blocking_io/http/mod.rs +++ b/gix-transport/src/client/blocking_io/http/mod.rs @@ -280,11 +280,9 @@ impl Transport { name.eq_ignore_ascii_case("content-type") && value.trim() == wanted_content_type }) }) { - return Err(client::Error::Http(Error::Detail { - description: format!( - "Didn't find '{wanted_content_type}' header to indicate 'smart' protocol, and 'dumb' protocol is not supported." - ), - })); + return Err(client::Error::HttpDetail(format!( + "Didn't find '{wanted_content_type}' header to indicate 'smart' protocol, and 'dumb' protocol is not supported." + ))); } Ok(()) } @@ -391,13 +389,11 @@ impl blocking_io::Transport for Transport { if let Some(announced_service) = line.as_bstr().strip_prefix(b"# service=") { if announced_service != service.as_str().as_bytes() { - return Err(client::Error::Http(Error::Detail { - description: format!( - "Expected to see service {:?}, but got {:?}", - service.as_str(), - announced_service - ), - })); + return Err(client::Error::HttpDetail(format!( + "Expected to see service {:?}, but got {:?}", + service.as_str(), + announced_service + ))); } line_reader.as_read().read_to_end(&mut Vec::new())?; diff --git a/gix-transport/src/client/blocking_io/http/redirect.rs b/gix-transport/src/client/blocking_io/http/redirect.rs index 08a5144e4b8..67be48a522f 100644 --- a/gix-transport/src/client/blocking_io/http/redirect.rs +++ b/gix-transport/src/client/blocking_io/http/redirect.rs @@ -1,22 +1,14 @@ /// The error provided when redirection went beyond what we deem acceptable. -#[derive(Debug, thiserror::Error)] -#[error("Redirect url {redirect_url:?} could not be reconciled with original url {expected_url} as they don't share the same suffix")] -pub struct Error { - redirect_url: String, - expected_url: String, -} +pub type Error = crate::Error; pub(crate) fn base_url(redirect_url: &str, base_url: &str, url: String) -> Result { let tail = url .strip_prefix(base_url) .expect("BUG: caller assures `base_url` is subset of `url`"); - redirect_url - .strip_suffix(tail) - .ok_or_else(|| Error { - redirect_url: redirect_url.into(), - expected_url: url, - }) - .map(ToOwned::to_owned) + redirect_url.strip_suffix(tail).ok_or_else(|| crate::Error::Redirect { + redirect_url: redirect_url.into(), + expected_url: url, + }).map(ToOwned::to_owned) } pub(crate) fn swap_tails(effective_base_url: Option<&str>, base_url: &str, mut url: String) -> String { diff --git a/gix-transport/src/client/blocking_io/http/reqwest/remote.rs b/gix-transport/src/client/blocking_io/http/reqwest/remote.rs index 50e1c36b2a7..eca364e619d 100644 --- a/gix-transport/src/client/blocking_io/http/reqwest/remote.rs +++ b/gix-transport/src/client/blocking_io/http/reqwest/remote.rs @@ -12,29 +12,7 @@ use crate::client::blocking_io::http::{ }; /// The error returned by the 'remote' helper, a purely internal construct to perform http requests. -#[derive(Debug, thiserror::Error)] -#[allow(missing_docs)] -pub enum Error { - #[error(transparent)] - Reqwest(#[from] reqwest::Error), - #[error("Could not finish reading all data to post to the remote")] - ReadPostBody(#[from] std::io::Error), - #[error("Request configuration failed")] - ConfigureRequest(#[from] Box), - #[error(transparent)] - Redirect(#[from] redirect::Error), -} - -impl crate::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { - match self { - Error::Reqwest(err) => { - err.is_timeout() || err.is_connect() || err.status().is_some_and(|status| status.is_server_error()) - } - _ => false, - } - } -} +pub type Error = crate::Error; impl Default for Remote { fn default() -> Self { @@ -122,7 +100,7 @@ impl Default for Remote { if let Some(ref mut request_options) = config.backend.as_ref().and_then(|backend| backend.lock().ok()) { if let Some(options) = request_options.downcast_mut::() { if let Some(configure_request) = &mut options.configure_request { - configure_request(&mut req)?; + configure_request(&mut req).map_err(|e| Error::ConfigureRequest(e.to_string()))?; } } } @@ -215,9 +193,7 @@ impl Remote { .expect("handler thread should never panic") .expect_err("something should have gone wrong with curl (we join on error only)"); *self = Remote::default(); - http::Error::InitHttpClient { - source: Box::new(err_that_brought_thread_down), - } + http::Error::InitHttpClient(err_that_brought_thread_down.to_string()) } fn make_request( diff --git a/gix-transport/src/client/blocking_io/http/traits.rs b/gix-transport/src/client/blocking_io/http/traits.rs index fd59ef957ca..cdb5f2f708e 100644 --- a/gix-transport/src/client/blocking_io/http/traits.rs +++ b/gix-transport/src/client/blocking_io/http/traits.rs @@ -1,39 +1,7 @@ use crate::client::WriteMode; /// The error used by the [Http] trait. -#[derive(Debug, thiserror::Error)] -#[allow(missing_docs)] -pub enum Error { - #[error("Could not initialize the http client")] - InitHttpClient { - source: Box, - }, - #[error("{description}")] - Detail { description: String }, - #[error("An IO error occurred while uploading the body of a POST request")] - PostBody(#[from] std::io::Error), -} - -impl crate::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { - match self { - Error::PostBody(err) => err.is_spurious(), - #[cfg(any(feature = "http-client-reqwest", feature = "http-client-curl"))] - Error::InitHttpClient { source } => { - #[cfg(feature = "http-client-curl")] - if let Some(err) = source.downcast_ref::() { - return err.is_spurious(); - } - #[cfg(feature = "http-client-reqwest")] - if let Some(err) = source.downcast_ref::() { - return err.is_spurious(); - } - false - } - _ => false, - } - } -} +pub type Error = crate::Error; /// The return value of [`Http::get()`]. pub struct GetResponse { diff --git a/gix-transport/src/client/blocking_io/ssh/mod.rs b/gix-transport/src/client/blocking_io/ssh/mod.rs index 4254ab38584..22f7b6e5920 100644 --- a/gix-transport/src/client/blocking_io/ssh/mod.rs +++ b/gix-transport/src/client/blocking_io/ssh/mod.rs @@ -8,16 +8,7 @@ use gix_url::{ArgumentSafety::*, Url}; use crate::{client::blocking_io::file::SpawnProcessOnDemand, Protocol}; /// The error used in [`connect()`]. -#[derive(Debug, thiserror::Error)] -#[allow(missing_docs)] -pub enum Error { - #[error("The scheme in \"{}\" is not usable for an ssh connection", .0.to_bstring())] - UnsupportedScheme(gix_url::Url), - #[error("Host name '{host}' could be mistaken for a command-line argument")] - AmbiguousHostName { host: String }, -} - -impl crate::IsSpuriousError for Error {} +pub type Error = crate::Error; /// The kind of SSH programs we have built-in support for. /// @@ -40,24 +31,8 @@ mod program_kind; /// pub mod invocation { - use std::ffi::OsString; - /// The error returned when producing ssh invocation arguments based on a selected invocation kind. - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error("Username '{user}' could be mistaken for a command-line argument")] - AmbiguousUserName { user: String }, - #[error("Host name '{host}' could be mistaken for a command-line argument")] - AmbiguousHostName { host: String }, - #[error("The 'Simple' ssh variant doesn't support {function}")] - Unsupported { - /// The simple command that should have been invoked. - command: OsString, - /// The function that was unsupported - function: &'static str, - }, - } + pub type Error = crate::Error; } /// @@ -109,7 +84,7 @@ pub fn connect( trace: bool, ) -> Result { if url.scheme != gix_url::Scheme::Ssh || url.host().is_none() { - return Err(Error::UnsupportedScheme(url)); + return Err(Error::UnsupportedScheme(url.scheme)); } let ssh_cmd = options.ssh_command(); let kind = determine_client_kind(options.kind, ssh_cmd, &url, options.disallow_shell)?; diff --git a/gix-transport/src/client/blocking_io/ssh/program_kind.rs b/gix-transport/src/client/blocking_io/ssh/program_kind.rs index 932b1163c41..30abf5f33aa 100644 --- a/gix-transport/src/client/blocking_io/ssh/program_kind.rs +++ b/gix-transport/src/client/blocking_io/ssh/program_kind.rs @@ -54,7 +54,7 @@ impl ProgramKind { } ProgramKind::Simple => { if url.port.is_some() { - return Err(ssh::invocation::Error::Unsupported { + return Err(ssh::invocation::Error::SshUnsupported { command: ssh_cmd.into(), function: "setting the port", }); diff --git a/gix-transport/src/client/blocking_io/ssh/tests.rs b/gix-transport/src/client/blocking_io/ssh/tests.rs index 3bffe1f0fc6..e3f16a1b2fd 100644 --- a/gix-transport/src/client/blocking_io/ssh/tests.rs +++ b/gix-transport/src/client/blocking_io/ssh/tests.rs @@ -205,7 +205,7 @@ mod program_kind { fn simple_cannot_handle_any_arguments() { assert!(matches!( try_call(ProgramKind::Simple, "ssh://user@host:42/p", Protocol::V2), - Err(ssh::invocation::Error::Unsupported { .. }) + Err(ssh::invocation::Error::SshUnsupported { .. }) )); assert_eq!( call_args(ProgramKind::Simple, "ssh://user@host/p", Protocol::V2), diff --git a/gix-transport/src/client/capabilities.rs b/gix-transport/src/client/capabilities.rs index 79bf34a6347..dba64bd7e22 100644 --- a/gix-transport/src/client/capabilities.rs +++ b/gix-transport/src/client/capabilities.rs @@ -4,24 +4,6 @@ use bstr::{BStr, BString, ByteSlice}; use crate::client; use crate::Protocol; -/// The error used in [`Capabilities::from_bytes()`] and [`Capabilities::from_lines()`]. -#[derive(Debug, thiserror::Error)] -#[allow(missing_docs)] -pub enum Error { - #[error("Capabilities were missing entirely as there was no 0 byte")] - MissingDelimitingNullByte, - #[error("there was not a single capability behind the delimiter")] - NoCapabilities, - #[error("a version line was expected, but none was retrieved")] - MissingVersionLine, - #[error("expected 'version X', got {0:?}")] - MalformattedVersionLine(BString), - #[error("Got unsupported version {actual:?}, expected {}", *desired as u8)] - UnsupportedVersion { desired: Protocol, actual: BString }, - #[error("An IO error occurred while reading V2 lines")] - Io(#[from] std::io::Error), -} - /// A structure to represent multiple [capabilities](Capability) or features supported by the server. /// /// ### Deviation @@ -82,10 +64,10 @@ impl Capabilities { /// Parse capabilities from the given `bytes`. /// /// Useful in case they are encoded within a `ref` behind a null byte. - pub fn from_bytes(bytes: &[u8]) -> Result<(Capabilities, usize), Error> { - let delimiter_pos = bytes.find_byte(0).ok_or(Error::MissingDelimitingNullByte)?; + pub fn from_bytes(bytes: &[u8]) -> Result<(Capabilities, usize), crate::Error> { + let delimiter_pos = bytes.find_byte(0).ok_or(crate::Error::MissingDelimitingNullByte)?; if delimiter_pos + 1 == bytes.len() { - return Err(Error::NoCapabilities); + return Err(crate::Error::NoCapabilities); } let capabilities = &bytes[delimiter_pos + 1..]; Ok(( @@ -103,19 +85,19 @@ impl Capabilities { /// Useful for parsing capabilities from a data sent from a server, and to avoid having to deal with /// blocking and async traits for as long as possible. There is no value in parsing a few bytes /// in a non-blocking fashion. - pub fn from_lines(lines_buf: BString) -> Result { + pub fn from_lines(lines_buf: BString) -> Result { let mut lines = <_ as bstr::ByteSlice>::lines(lines_buf.as_slice().trim()); - let version_line = lines.next().ok_or(Error::MissingVersionLine)?; + let version_line = lines.next().ok_or(crate::Error::MissingVersionLine)?; let (name, value) = version_line.split_at( version_line .find(b" ") - .ok_or_else(|| Error::MalformattedVersionLine(version_line.to_owned().into()))?, + .ok_or_else(|| crate::Error::MalformattedVersionLine(version_line.to_owned().into()))?, ); if name != b"version" { - return Err(Error::MalformattedVersionLine(version_line.to_owned().into())); + return Err(crate::Error::MalformattedVersionLine(version_line.to_owned().into())); } if value != b" 2" { - return Err(Error::UnsupportedVersion { + return Err(crate::Error::UnsupportedVersion { desired: Protocol::V2, actual: value.to_owned().into(), }); diff --git a/gix-transport/src/client/git/blocking_io.rs b/gix-transport/src/client/git/blocking_io.rs index 92fe5b61a95..cec8e530e9b 100644 --- a/gix-transport/src/client/git/blocking_io.rs +++ b/gix-transport/src/client/git/blocking_io.rs @@ -179,23 +179,7 @@ pub mod connect { use crate::client::git; /// The error used in [`connect()`]. - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error("An IO error occurred when connecting to the server")] - Io(#[from] std::io::Error), - #[error("Could not parse {host:?} as virtual host with format [:port]")] - VirtualHostInvalid { host: String }, - } - - impl crate::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { - match self { - Error::Io(err) => err.is_spurious(), - _ => false, - } - } - } + pub type Error = crate::Error; fn parse_host(input: String) -> Result<(String, Option), Error> { let mut tokens = input.splitn(2, ':'); @@ -203,7 +187,7 @@ pub mod connect { (Some(host), None) => (host.to_owned(), None), (Some(host), Some(port)) => ( host.to_owned(), - Some(port.parse().map_err(|_| Error::VirtualHostInvalid { host: input })?), + Some(port.parse().map_err(|_| crate::Error::VirtualHostInvalid { host: input })?), ), _ => unreachable!("we expect at least one token, the original string"), }) diff --git a/gix-transport/src/client/non_io_types.rs b/gix-transport/src/client/non_io_types.rs index 98f79020a46..14f1cd30f04 100644 --- a/gix-transport/src/client/non_io_types.rs +++ b/gix-transport/src/client/non_io_types.rs @@ -48,116 +48,8 @@ pub(crate) mod connect { /// The error used in `connect()`. /// /// (Both blocking and async I/O use the same error type.) - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error(transparent)] - Url(#[from] gix_url::parse::Error), - #[error("The git repository path could not be converted to UTF8")] - PathConversion(#[from] bstr::Utf8Error), - #[error("connection failed")] - Connection(#[from] Box), - #[error("The url {url:?} contains information that would not be used by the {scheme} protocol")] - UnsupportedUrlTokens { - url: bstr::BString, - scheme: gix_url::Scheme, - }, - #[error("The '{0}' protocol is currently unsupported")] - UnsupportedScheme(gix_url::Scheme), - #[cfg(not(any(feature = "http-client-curl", feature = "http-client-reqwest")))] - #[error( - "'{0}' is not compiled in. Compile with the 'http-client-curl' or 'http-client-reqwest' cargo feature" - )] - CompiledWithoutHttp(gix_url::Scheme), - } - - // TODO: maybe fix this workaround: want `IsSpuriousError` in `Connection(…)` - impl crate::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { - match self { - Error::Connection(err) => { - #[cfg(feature = "blocking-client")] - if let Some(err) = err.downcast_ref::() { - return err.is_spurious(); - } - if let Some(err) = err.downcast_ref::() { - return err.is_spurious(); - } - false - } - _ => false, - } - } - } -} - -mod error { - use std::ffi::OsString; - - use bstr::BString; - - #[cfg(feature = "http-client")] - use crate::client::blocking_io::http; - #[cfg(feature = "blocking-client")] - use crate::client::blocking_io::ssh; - use crate::client::capabilities; - - #[cfg(feature = "http-client")] - type HttpError = http::Error; - #[cfg(feature = "blocking-client")] - type SshInvocationError = ssh::invocation::Error; - #[cfg(not(feature = "http-client"))] - type HttpError = std::convert::Infallible; - #[cfg(not(feature = "blocking-client"))] - type SshInvocationError = std::convert::Infallible; - - /// The error used in most methods of the [`client`][crate::client] module - #[derive(thiserror::Error, Debug)] - #[allow(missing_docs)] - pub enum Error { - #[error("A request was performed without performing the handshake first")] - MissingHandshake, - #[error("An IO error occurred when talking to the server")] - Io(#[from] std::io::Error), - #[error("Capabilities could not be parsed")] - Capabilities { - #[from] - err: capabilities::Error, - }, - #[error("A packet line could not be decoded")] - LineDecode { - #[from] - err: gix_packetline::decode::Error, - }, - #[error("A {0} line was expected, but there was none")] - ExpectedLine(&'static str), - #[error("Expected a data line, but got a delimiter")] - ExpectedDataLine, - #[error("The transport layer does not support authentication")] - AuthenticationUnsupported, - #[error("The transport layer refuses to use a given identity: {0}")] - AuthenticationRefused(&'static str), - #[error("The protocol version indicated by {:?} is unsupported", {0})] - UnsupportedProtocolVersion(BString), - #[error("Failed to invoke program {command:?}")] - InvokeProgram { source: std::io::Error, command: OsString }, - #[error(transparent)] - Http(#[from] HttpError), - #[error(transparent)] - SshInvocation(SshInvocationError), - #[error("The repository path '{path}' could be mistaken for a command-line argument")] - AmbiguousPath { path: BString }, - } - - impl crate::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { - match self { - Error::Io(err) => err.is_spurious(), - Error::Http(err) => err.is_spurious(), - _ => false, - } - } - } + pub type Error = crate::Error; } -pub use error::Error; +/// The error used in most methods of the [`client`][crate::client] module. +pub type Error = crate::Error; diff --git a/gix-transport/src/error.rs b/gix-transport/src/error.rs new file mode 100644 index 00000000000..2c323139ddc --- /dev/null +++ b/gix-transport/src/error.rs @@ -0,0 +1,276 @@ +use std::fmt; + +use bstr::BString; + +/// The main error type for `gix-transport`, with variants that indicate retry-ability. +#[derive(Debug)] +pub enum Error { + /// An IO error occurred. + Io(std::io::Error), + /// A URL could not be parsed. + UrlParse(gix_url::parse::Error), + /// UTF-8 conversion failed for a path. + PathConversion(bstr::Utf8Error), + /// The scheme is unsupported. + UnsupportedScheme(gix_url::Scheme), + /// The URL contains tokens that are incompatible with the scheme. + UnsupportedUrlTokens { + /// The URL that was problematic. + url: BString, + /// The scheme being used. + scheme: gix_url::Scheme, + }, + /// The repository path could be mistaken for a command-line argument. + AmbiguousPath { + /// The path that is ambiguous. + path: BString, + }, + /// A host name could be mistaken for a command-line argument. + AmbiguousHostName { + /// The host name that is ambiguous. + host: String, + }, + /// A user name could be mistaken for a command-line argument. + AmbiguousUserName { + /// The user name that is ambiguous. + user: String, + }, + /// The virtual host specification was invalid. + VirtualHostInvalid { + /// The invalid host string. + host: String, + }, + /// A request was performed without performing the handshake first. + MissingHandshake, + /// Capabilities could not be parsed. + Capabilities(String), + /// A packet line could not be decoded. + LineDecode(String), + /// A specific line was expected but missing. + ExpectedLine(&'static str), + /// Expected a data line, but got a delimiter. + ExpectedDataLine, + /// The transport layer does not support authentication. + AuthenticationUnsupported, + /// The transport layer refuses to use a given identity. + AuthenticationRefused(&'static str), + /// The protocol version is unsupported. + UnsupportedProtocolVersion(BString), + /// Failed to invoke a program. + InvokeProgram { + /// The command that failed. + command: std::ffi::OsString, + /// The underlying IO error. + source: std::io::Error, + }, + /// Could not initialize the HTTP client. + InitHttpClient(String), + /// An error occurred while uploading the body of a POST request. + PostBody(std::io::Error), + /// HTTP transport error with details. + HttpDetail(String), + /// Redirect URL could not be reconciled. + Redirect { + /// The redirect URL. + redirect_url: String, + /// The expected URL. + expected_url: String, + }, + /// Could not finish reading all data to post to the remote. + ReadPostBody(std::io::Error), + /// Request configuration failed. + ConfigureRequest(String), + /// Authentication failed. + Authenticate(String), + /// A 'Simple' SSH variant doesn't support a particular function. + SshUnsupported { + /// The simple command that should have been invoked. + command: std::ffi::OsString, + /// The function that was unsupported. + function: &'static str, + }, + /// Capabilities were missing entirely as there was no 0 byte. + MissingDelimitingNullByte, + /// There was not a single capability behind the delimiter. + NoCapabilities, + /// A version line was expected, but none was retrieved. + MissingVersionLine, + /// Expected 'version X', got something else. + MalformattedVersionLine(BString), + /// Got unsupported version. + UnsupportedVersion { + /// The desired protocol version. + desired: crate::Protocol, + /// The actual version received. + actual: BString, + }, + /// An HTTP feature is not compiled in. + #[cfg(not(any(feature = "http-client-curl", feature = "http-client-reqwest")))] + CompiledWithoutHttp(gix_url::Scheme), + /// Connection failed with a nested error. + Connection(String), + /// A curl error occurred. + #[cfg(feature = "http-client-curl")] + Curl(curl::Error), + /// A reqwest error occurred. + #[cfg(feature = "http-client-reqwest")] + Reqwest(reqwest::Error), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Io(err) => write!(f, "IO error: {err}"), + Error::UrlParse(err) => write!(f, "URL parse error: {err}"), + Error::PathConversion(err) => write!(f, "The git repository path could not be converted to UTF8: {err}"), + Error::UnsupportedScheme(scheme) => write!(f, "The '{scheme}' protocol is currently unsupported"), + Error::UnsupportedUrlTokens { url, scheme } => { + write!(f, "The url {url:?} contains information that would not be used by the {scheme} protocol") + } + Error::AmbiguousPath { path } => { + write!(f, "The repository path '{path}' could be mistaken for a command-line argument") + } + Error::AmbiguousHostName { host } => { + write!(f, "Host name '{host}' could be mistaken for a command-line argument") + } + Error::AmbiguousUserName { user } => { + write!(f, "Username '{user}' could be mistaken for a command-line argument") + } + Error::VirtualHostInvalid { host } => { + write!(f, "Could not parse {host:?} as virtual host with format [:port]") + } + Error::MissingHandshake => write!(f, "A request was performed without performing the handshake first"), + Error::Capabilities(msg) => write!(f, "Capabilities could not be parsed: {msg}"), + Error::LineDecode(msg) => write!(f, "A packet line could not be decoded: {msg}"), + Error::ExpectedLine(line_type) => write!(f, "A {line_type} line was expected, but there was none"), + Error::ExpectedDataLine => write!(f, "Expected a data line, but got a delimiter"), + Error::AuthenticationUnsupported => write!(f, "The transport layer does not support authentication"), + Error::AuthenticationRefused(reason) => write!(f, "The transport layer refuses to use a given identity: {reason}"), + Error::UnsupportedProtocolVersion(version) => { + write!(f, "The protocol version indicated by {version:?} is unsupported") + } + Error::InvokeProgram { command, source } => write!(f, "Failed to invoke program {command:?}: {source}"), + Error::InitHttpClient(msg) => write!(f, "Could not initialize the http client: {msg}"), + Error::PostBody(err) => write!(f, "An IO error occurred while uploading the body of a POST request: {err}"), + Error::HttpDetail(description) => write!(f, "{description}"), + Error::Redirect { redirect_url, expected_url } => { + write!( + f, + "Redirect url {redirect_url:?} could not be reconciled with original url {expected_url} as they don't share the same suffix" + ) + } + Error::ReadPostBody(err) => write!(f, "Could not finish reading all data to post to the remote: {err}"), + Error::ConfigureRequest(msg) => write!(f, "Request configuration failed: {msg}"), + Error::Authenticate(msg) => write!(f, "Authentication failed: {msg}"), + Error::SshUnsupported { command, function } => { + write!(f, "The 'Simple' ssh variant doesn't support {function}: {command:?}") + } + Error::MissingDelimitingNullByte => write!(f, "Capabilities were missing entirely as there was no 0 byte"), + Error::NoCapabilities => write!(f, "there was not a single capability behind the delimiter"), + Error::MissingVersionLine => write!(f, "a version line was expected, but none was retrieved"), + Error::MalformattedVersionLine(line) => write!(f, "expected 'version X', got {line:?}"), + Error::UnsupportedVersion { desired, actual } => { + write!(f, "Got unsupported version {actual:?}, expected {}", *desired as u8) + } + #[cfg(not(any(feature = "http-client-curl", feature = "http-client-reqwest")))] + Error::CompiledWithoutHttp(scheme) => { + write!( + f, + "'{scheme}' is not compiled in. Compile with the 'http-client-curl' or 'http-client-reqwest' cargo feature" + ) + } + Error::Connection(msg) => write!(f, "connection failed: {msg}"), + #[cfg(feature = "http-client-curl")] + Error::Curl(err) => write!(f, "curl error: {err}"), + #[cfg(feature = "http-client-reqwest")] + Error::Reqwest(err) => write!(f, "reqwest error: {err}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => Some(err), + Error::UrlParse(err) => Some(err), + Error::PathConversion(err) => Some(err), + Error::InvokeProgram { source, .. } => Some(source), + Error::PostBody(err) => Some(err), + Error::ReadPostBody(err) => Some(err), + #[cfg(feature = "http-client-curl")] + Error::Curl(err) => Some(err), + #[cfg(feature = "http-client-reqwest")] + Error::Reqwest(err) => Some(err), + _ => None, + } + } +} + +impl crate::IsSpuriousError for Error { + fn is_spurious(&self) -> bool { + match self { + Error::Io(err) => err.is_spurious(), + Error::PostBody(err) => err.is_spurious(), + Error::ReadPostBody(err) => err.is_spurious(), + Error::InvokeProgram { source, .. } => source.is_spurious(), + #[cfg(feature = "http-client-curl")] + Error::Curl(err) => crate::client::blocking_io::http::curl::curl_is_spurious(err), + #[cfg(feature = "http-client-reqwest")] + Error::Reqwest(err) => { + err.is_timeout() || err.is_connect() || err.status().is_some_and(|status| status.is_server_error()) + } + _ => false, + } + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: gix_url::parse::Error) -> Self { + Error::UrlParse(err) + } +} + +impl From for Error { + fn from(err: bstr::Utf8Error) -> Self { + Error::PathConversion(err) + } +} + +impl From for Error { + fn from(err: gix_packetline::decode::Error) -> Self { + Error::LineDecode(err.to_string()) + } +} + +#[cfg(feature = "http-client-curl")] +impl From for Error { + fn from(err: curl::Error) -> Self { + Error::Curl(err) + } +} + +#[cfg(feature = "http-client-reqwest")] +impl From for Error { + fn from(err: reqwest::Error) -> Self { + Error::Reqwest(err) + } +} + +#[cfg(feature = "blocking-client")] +impl From for Error { + fn from(err: gix_credentials::protocol::Error) -> Self { + Error::Authenticate(err.to_string()) + } +} + +impl From> for Error { + fn from(err: Box) -> Self { + Error::Connection(err.to_string()) + } +} diff --git a/gix-transport/src/lib.rs b/gix-transport/src/lib.rs index e29026a252e..945dd68ef24 100644 --- a/gix-transport/src/lib.rs +++ b/gix-transport/src/lib.rs @@ -82,5 +82,8 @@ mod traits { } pub use traits::IsSpuriousError; +mod error; +pub use error::Error; + /// pub mod client;