gix_protocol/fetch/response/
mod.rs

1use bstr::BString;
2use gix_transport::{client, Protocol};
3
4use crate::{command::Feature, fetch::Response};
5
6/// The error returned in the [response module][crate::fetch::response].
7#[derive(Debug, thiserror::Error)]
8#[allow(missing_docs)]
9pub enum Error {
10    #[error("Failed to read from line reader")]
11    Io(#[source] std::io::Error),
12    #[error(transparent)]
13    UploadPack(#[from] gix_transport::packetline::read::Error),
14    #[error(transparent)]
15    Transport(#[from] client::Error),
16    #[error("Currently we require feature {feature:?}, which is not supported by the server")]
17    MissingServerCapability { feature: &'static str },
18    #[error("Encountered an unknown line prefix in {line:?}")]
19    UnknownLineType { line: String },
20    #[error("Unknown or unsupported header: {header:?}")]
21    UnknownSectionHeader { header: String },
22}
23
24impl From<std::io::Error> for Error {
25    fn from(err: std::io::Error) -> Self {
26        if err.kind() == std::io::ErrorKind::Other {
27            match err.into_inner() {
28                Some(err) => match err.downcast::<gix_transport::packetline::read::Error>() {
29                    Ok(err) => Error::UploadPack(*err),
30                    Err(err) => Error::Io(std::io::Error::new(std::io::ErrorKind::Other, err)),
31                },
32                None => Error::Io(std::io::ErrorKind::Other.into()),
33            }
34        } else {
35            Error::Io(err)
36        }
37    }
38}
39
40impl gix_transport::IsSpuriousError for Error {
41    fn is_spurious(&self) -> bool {
42        match self {
43            Error::Io(err) => err.is_spurious(),
44            Error::Transport(err) => err.is_spurious(),
45            _ => false,
46        }
47    }
48}
49
50/// An 'ACK' line received from the server.
51#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
53pub enum Acknowledgement {
54    /// The contained `id` is in common.
55    Common(gix_hash::ObjectId),
56    /// The server is ready to receive more lines.
57    Ready,
58    /// The server isn't ready yet.
59    Nak,
60}
61
62pub use gix_shallow::Update as ShallowUpdate;
63
64/// A wanted-ref line received from the server.
65#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
66#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
67pub struct WantedRef {
68    /// The object id of the wanted ref, as seen by the server.
69    pub id: gix_hash::ObjectId,
70    /// The name of the ref, as requested by the client as a `want-ref` argument.
71    pub path: BString,
72}
73
74/// Parse a `ShallowUpdate` from a `line` as received to the server.
75pub fn shallow_update_from_line(line: &str) -> Result<ShallowUpdate, Error> {
76    match line.trim_end().split_once(' ') {
77        Some((prefix, id)) => {
78            let id = gix_hash::ObjectId::from_hex(id.as_bytes())
79                .map_err(|_| Error::UnknownLineType { line: line.to_owned() })?;
80            Ok(match prefix {
81                "shallow" => ShallowUpdate::Shallow(id),
82                "unshallow" => ShallowUpdate::Unshallow(id),
83                _ => return Err(Error::UnknownLineType { line: line.to_owned() }),
84            })
85        }
86        None => Err(Error::UnknownLineType { line: line.to_owned() }),
87    }
88}
89
90impl Acknowledgement {
91    /// Parse an `Acknowledgement` from a `line` as received to the server.
92    pub fn from_line(line: &str) -> Result<Acknowledgement, Error> {
93        let mut tokens = line.trim_end().splitn(3, ' ');
94        match (tokens.next(), tokens.next(), tokens.next()) {
95            (Some(first), id, description) => Ok(match first {
96                "ready" => Acknowledgement::Ready, // V2
97                "NAK" => Acknowledgement::Nak,     // V1
98                "ACK" => {
99                    let id = match id {
100                        Some(id) => gix_hash::ObjectId::from_hex(id.as_bytes())
101                            .map_err(|_| Error::UnknownLineType { line: line.to_owned() })?,
102                        None => return Err(Error::UnknownLineType { line: line.to_owned() }),
103                    };
104                    if let Some(description) = description {
105                        match description {
106                            "common" => {}
107                            "ready" => return Ok(Acknowledgement::Ready),
108                            _ => return Err(Error::UnknownLineType { line: line.to_owned() }),
109                        }
110                    }
111                    Acknowledgement::Common(id)
112                }
113                _ => return Err(Error::UnknownLineType { line: line.to_owned() }),
114            }),
115            (None, _, _) => Err(Error::UnknownLineType { line: line.to_owned() }),
116        }
117    }
118    /// Returns the hash of the acknowledged object if this instance acknowledges a common one.
119    pub fn id(&self) -> Option<&gix_hash::ObjectId> {
120        match self {
121            Acknowledgement::Common(id) => Some(id),
122            _ => None,
123        }
124    }
125}
126
127impl WantedRef {
128    /// Parse a `WantedRef` from a `line` as received from the server.
129    pub fn from_line(line: &str) -> Result<WantedRef, Error> {
130        match line.trim_end().split_once(' ') {
131            Some((id, path)) => {
132                let id = gix_hash::ObjectId::from_hex(id.as_bytes())
133                    .map_err(|_| Error::UnknownLineType { line: line.to_owned() })?;
134                Ok(WantedRef { id, path: path.into() })
135            }
136            None => Err(Error::UnknownLineType { line: line.to_owned() }),
137        }
138    }
139}
140
141impl Response {
142    /// Return true if the response has a pack which can be read next.
143    pub fn has_pack(&self) -> bool {
144        self.has_pack
145    }
146
147    /// Return an error if the given `features` don't contain the required ones (the ones this implementation needs)
148    /// for the given `version` of the protocol.
149    ///
150    /// Even though technically any set of features supported by the server could work, we only implement the ones that
151    /// make it easy to maintain all versions with a single code base that aims to be and remain maintainable.
152    pub fn check_required_features(version: Protocol, features: &[Feature]) -> Result<(), Error> {
153        match version {
154            Protocol::V0 | Protocol::V1 => {
155                let has = |name: &str| features.iter().any(|f| f.0 == name);
156                // Let's focus on V2 standards, and simply not support old servers to keep our code simpler
157                if !has("multi_ack_detailed") {
158                    return Err(Error::MissingServerCapability {
159                        feature: "multi_ack_detailed",
160                    });
161                }
162                // It's easy to NOT do sideband for us, but then again, everyone supports it.
163                // CORRECTION: If side-band is off, it would send the packfile without packet line encoding,
164                // which is nothing we ever want to deal with (despite it being more efficient). In V2, this
165                // is not even an option anymore, sidebands are always present.
166                if !has("side-band") && !has("side-band-64k") {
167                    return Err(Error::MissingServerCapability {
168                        feature: "side-band OR side-band-64k",
169                    });
170                }
171            }
172            Protocol::V2 => {}
173        }
174        Ok(())
175    }
176
177    /// Return all acknowledgements [parsed previously][Response::from_line_reader()].
178    pub fn acknowledgements(&self) -> &[Acknowledgement] {
179        &self.acks
180    }
181
182    /// Return all shallow update lines [parsed previously][Response::from_line_reader()].
183    pub fn shallow_updates(&self) -> &[ShallowUpdate] {
184        &self.shallows
185    }
186
187    /// Append the given `updates` which may have been obtained from a
188    /// (handshake::Outcome)[crate::handshake::Outcome::v1_shallow_updates].
189    ///
190    /// In V2, these are received as part of the pack, but V1 sends them early, so we
191    /// offer to re-integrate them here.
192    pub fn append_v1_shallow_updates(&mut self, updates: Option<Vec<ShallowUpdate>>) {
193        self.shallows.extend(updates.into_iter().flatten());
194    }
195
196    /// Return all wanted-refs [parsed previously][Response::from_line_reader()].
197    pub fn wanted_refs(&self) -> &[WantedRef] {
198        &self.wanted_refs
199    }
200}
201
202#[cfg(any(feature = "async-client", feature = "blocking-client"))]
203impl Response {
204    /// with a friendly server, we just assume that a non-ack line is a pack line
205    /// which is our hint to stop here.
206    fn parse_v1_ack_or_shallow_or_assume_pack(
207        acks: &mut Vec<Acknowledgement>,
208        shallows: &mut Vec<ShallowUpdate>,
209        peeked_line: &str,
210    ) -> bool {
211        match Acknowledgement::from_line(peeked_line) {
212            Ok(ack) => match ack.id() {
213                Some(id) => {
214                    if !acks.iter().any(|a| a.id() == Some(id)) {
215                        acks.push(ack);
216                    }
217                }
218                None => acks.push(ack),
219            },
220            Err(_) => match shallow_update_from_line(peeked_line) {
221                Ok(shallow) => {
222                    shallows.push(shallow);
223                }
224                Err(_) => return true,
225            },
226        }
227        false
228    }
229}
230
231#[cfg(feature = "async-client")]
232mod async_io;
233#[cfg(feature = "blocking-client")]
234mod blocking_io;