1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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<dyn std::error::Error + Send + Sync + 'static>,
    },
    #[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::<crate::client::http::curl::Error>() {
                    return err.is_spurious();
                };
                #[cfg(feature = "http-client-reqwest")]
                if let Some(err) = source.downcast_ref::<crate::client::http::reqwest::remote::Error>() {
                    return err.is_spurious();
                };
                false
            }
            _ => false,
        }
    }
}

/// The return value of [`Http::get()`].
pub struct GetResponse<H, B> {
    /// The response headers.
    pub headers: H,
    /// The response body.
    pub body: B,
}

/// The return value of [`Http::post()`].
pub struct PostResponse<H, B, PB> {
    /// The body to post to the server as part of the request.
    ///
    /// **Note**: Implementations should drop the handle to avoid deadlocks.
    pub post_body: PB,
    /// The headers of the post response.
    pub headers: H,
    /// The body of the post response.
    pub body: B,
}

/// Whether or not the post body is expected to fit into memory or not.
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
pub enum PostBodyDataKind {
    /// We know how much data we are sending and think it will fit into memory. This allows to collect it into a buffer
    /// and send it with `Content-Length: <body-len>`.
    BoundedAndFitsIntoMemory,
    /// We don't know how much data we will send and assume it won't fit into memory. This enables streaming mode.
    Unbounded,
}

impl From<WriteMode> for PostBodyDataKind {
    fn from(m: WriteMode) -> Self {
        match m {
            WriteMode::Binary => PostBodyDataKind::Unbounded,
            WriteMode::OneLfTerminatedLinePerWriteCall => PostBodyDataKind::BoundedAndFitsIntoMemory,
        }
    }
}

impl<A, B, C> From<PostResponse<A, B, C>> for GetResponse<A, B> {
    fn from(v: PostResponse<A, B, C>) -> Self {
        GetResponse {
            headers: v.headers,
            body: v.body,
        }
    }
}

/// A trait to abstract the HTTP operations needed to power all git interactions: read via GET and write via POST.
/// Note that 401 must be turned into `std::io::Error(PermissionDenied)`, and other non-success http statuses must be transformed
/// into `std::io::Error(Other)`
#[allow(clippy::type_complexity)]
pub trait Http {
    /// A type providing headers line by line.
    type Headers: std::io::BufRead + Unpin;
    /// A type providing the response.
    type ResponseBody: std::io::BufRead;
    /// A type allowing to write the content to post.
    type PostBody: std::io::Write;

    /// Initiate a `GET` request to `url` provided the given `headers`, where `base_url` is so that `base_url + tail == url`.
    ///
    /// The `base_url` helps to validate redirects and to swap it with the effective base after a redirect.
    ///
    /// The `headers` are provided verbatim and include both the key as well as the value.
    fn get(
        &mut self,
        url: &str,
        base_url: &str,
        headers: impl IntoIterator<Item = impl AsRef<str>>,
    ) -> Result<GetResponse<Self::Headers, Self::ResponseBody>, Error>;

    /// Initiate a `POST` request to `url` providing with the given `headers`, where `base_url` is so that `base_url + tail == url`.
    ///
    /// The `base_url` helps to validate redirects and to swap it with the effective base after a redirect.
    ///
    /// The `headers` are provided verbatim and include both the key as well as the value.
    /// Note that the [`PostResponse`] contains the [`post_body`][PostResponse::post_body] field which implements [`std::io::Write`]
    /// and is expected to receive the body to post to the server. **It must be dropped** before reading the response
    /// to prevent deadlocks.
    fn post(
        &mut self,
        url: &str,
        base_url: &str,
        headers: impl IntoIterator<Item = impl AsRef<str>>,
        body: PostBodyDataKind,
    ) -> Result<PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, Error>;

    /// Pass `config` which can deserialize in the implementation's configuration, as documented separately.
    ///
    /// The caller must know how that `config` data looks like for the intended implementation.
    fn configure(
        &mut self,
        config: &dyn std::any::Any,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>;
}