anni_fetch/
client.rs

1use crate::io;
2use std::io::{Read, Cursor};
3use thiserror::Error;
4
5#[derive(Error, Debug)]
6pub enum ClientError {
7    // Common request errors
8    #[error("invalid server status")]
9    InvalidServerStatus,
10    #[error("invalid response content type, expected {0}, got {1}")]
11    InvalidContentType(&'static str, String),
12
13    // ls-ref error
14    #[error("invalid ref hash")]
15    InvalidRefHash,
16
17    #[error(transparent)]
18    RequestError(#[from] ureq::Error),
19    #[error(transparent)]
20    IOError(#[from] std::io::Error),
21    #[error(transparent)]
22    Utf8Error(#[from] std::string::FromUtf8Error),
23}
24
25pub struct Client {
26    url: String,
27    client: ureq::Agent,
28}
29
30impl Client {
31    pub fn new(url: &str) -> Self {
32        Self {
33            url: url.to_owned(),
34            client: ureq::AgentBuilder::new()
35                .user_agent("anni-fetch 0.2.0")
36                .build(),
37        }
38    }
39
40    pub fn handshake(&mut self) -> Result<PktIter, ClientError> {
41        let reader = self.client
42            .get(&format!("{}/info/refs?service=git-upload-pack", &self.url))
43            .set("Git-Protocol", "version=2")
44            .call()?
45            .into_reader();
46        Ok(PktIter::new(reader))
47    }
48
49    /// Use [Client::request] instead
50    #[deprecated]
51    pub fn command(&self, command: &str, capabilities: Option<&[(&str, Option<&[&str]>)]>, arguments: &[&str]) -> Result<impl Read + Send, ClientError> {
52        let out = Vec::new();
53        let mut cursor = std::io::Cursor::new(out);
54        io::write_pktline(&mut cursor, &format!("command={}", command))?;
55        io::write_pktline(&mut cursor, "object-format=sha1")?;
56        io::write_pktline(&mut cursor, "agent=git/2.28.0")?;
57
58        if let Some(capabilities) = capabilities {
59            for (k, v) in capabilities {
60                if let Some(v) = v {
61                    io::write_pktline(&mut cursor, &format!("{}={}", k, v.join(" ")))?;
62                } else {
63                    io::write_pktline(&mut cursor, k)?;
64                }
65            }
66        }
67        io::write_packet(&mut cursor, 1)?;
68
69        for arg in arguments.iter() {
70            io::write_pktline(&mut cursor, arg)?;
71        }
72        io::write_packet(&mut cursor, 0)?;
73
74        Ok(self.client
75            .post(&format!("{}/git-upload-pack", &self.url))
76            .set("Git-Protocol", "version=2")
77            .set("Content-Type", "application/x-git-upload-pack-request")
78            .set("Accept", "application/x-git-upload-pack-result")
79            .send_bytes(&cursor.into_inner())?
80            .into_reader())
81    }
82
83    pub fn request(&self, body: Vec<u8>) -> Result<PktIter, ClientError> {
84        let response = self.client
85            .post(&format!("{}/git-upload-pack", &self.url))
86            .set("Git-Protocol", "version=2")
87            .set("Content-Type", "application/x-git-upload-pack-request")
88            .set("Accept", "application/x-git-upload-pack-result")
89            .send_bytes(&body)?;
90        if response.status() != 200 {
91            return Err(ClientError::InvalidServerStatus);
92        } else if response.content_type() != "application/x-git-upload-pack-result" {
93            return Err(ClientError::InvalidContentType("application/x-git-upload-pack-result", response.content_type().to_owned()));
94        }
95        let reader = response.into_reader();
96        Ok(PktIter::new(reader))
97    }
98
99    pub fn ls_ref(&self, prefix: &str) -> Result<String, ClientError> {
100        let iter = self.request(
101            RequestBuilder::new(true)
102                .command("ls-refs")
103                .argument("peel")
104                .argument("symrefs")
105                .argument(&format!("ref-prefix {}", prefix))
106                .build()
107        )?;
108        for msg in iter {
109            match msg {
110                Message::Normal(mut n) => {
111                    n.truncate(40);
112                    return Ok(String::from_utf8(n)?);
113                }
114                _ => {}
115            }
116        }
117        Err(ClientError::InvalidRefHash)
118    }
119
120    /// Use [RequestBuilder::want] with [Client::ls_ref] instead
121    #[deprecated]
122    pub fn want_ref(&self, prefix: &str) -> Result<String, ClientError> {
123        Ok(format!("want {}", self.ls_ref(prefix)?))
124    }
125}
126
127/// Builder for pktline-based git request body
128pub struct RequestBuilder {
129    inner: Cursor<Vec<u8>>,
130    delimeter_written: bool,
131    flush_written: bool,
132}
133
134impl RequestBuilder {
135    /// Create a new RequestBuilder
136    ///
137    /// If auto_packet is enabled, DelimeterPacket would be inserted when first you call [argument],
138    /// and FlushPacket would be inserted at [build] time.
139    pub fn new(auto_packet: bool) -> Self {
140        let mut inner = Default::default();
141        io::write_pktline(&mut inner, "object-format=sha1").unwrap();
142        io::write_pktline(&mut inner, "agent=git/2.28.0").unwrap();
143        Self {
144            inner,
145            delimeter_written: !auto_packet,
146            flush_written: !auto_packet,
147        }
148    }
149
150    /// Write `command={command}` to body
151    pub fn command(mut self, command: &str) -> Self {
152        io::write_pktline(&mut self.inner, &format!("command={}", command)).unwrap();
153        self
154    }
155
156    /// Write `{name}={value_1} {value_2} {value_3}` to body
157    pub fn capability(mut self, name: &str, value: &[&str]) -> Self {
158        if value.len() != 0 {
159            io::write_pktline(&mut self.inner, &format!("{}={}", name, value.join(" "))).unwrap();
160        } else {
161            io::write_pktline(&mut self.inner, name).unwrap();
162        }
163        self
164    }
165
166    /// Write `{arg}` to body
167    pub fn argument(mut self, arg: &str) -> Self {
168        if !self.delimeter_written {
169            self = self.packet(Message::Delimeter);
170            self.delimeter_written = true;
171        }
172
173        io::write_pktline(&mut self.inner, arg).unwrap();
174        self
175    }
176
177    /// Write `Message::Flush` or `Message::Delimeter` to body
178    pub fn packet(mut self, packet: Message) -> Self {
179        match packet {
180            Message::Flush => io::write_packet(&mut self.inner, 0).unwrap(),
181            Message::Delimeter => io::write_packet(&mut self.inner, 1).unwrap(),
182            _ => panic!("invalid request packet type")
183        }
184        self
185    }
186
187    /// Write `want {hash}` to body
188    pub fn want(self, hash: &str) -> Self {
189        self.argument(&format!("want {}", hash))
190    }
191
192    /// Write `have {hash}` to body
193    pub fn have(self, hash: &str) -> Self {
194        self.argument(&format!("have {}", hash))
195    }
196
197    /// Build RequestBuilder into Vec<u8>
198    pub fn build(mut self) -> Vec<u8> {
199        if !self.flush_written {
200            self = self.packet(Message::Flush);
201            self.flush_written = true;
202        }
203        self.inner.into_inner()
204    }
205}
206
207/// Message abstracts the type of information you may receive from a Git server.
208#[derive(Debug, PartialEq)]
209pub enum Message {
210    Normal(Vec<u8>),
211    /// 0000 Flush Packet(flush-pkt)
212    ///
213    /// Indicates the end of a message
214    Flush,
215    /// 0001 Delimeter Packet(delim-pkt)
216    ///
217    /// Separates sections of a message
218    Delimeter,
219    /// 0002 Response End Packet(response-end-pkg)
220    ///
221    /// Indicates the end of a response for stateless connections
222    ResponseEnd,
223    /// Received when data is `packfile\n`
224    ///
225    /// After this message, only `Pack.+` messages would be sent
226    ///
227    /// There is a byte at the beginning of all `Pack.+` messages except PackStart
228    /// The stream code can be one of:
229    /// 1 - pack data
230    /// 2 - progress messages
231    /// 3 - fatal error message just before stream aborts
232    PackStart,
233    /// Received after `Message::PackStart` when stream code is 1
234    ///
235    /// Data of PACK file
236    PackData(Vec<u8>),
237    /// Received after `Message::PackStart` when stream code is 2
238    ///
239    /// Progress messages of the transfer
240    PackProgress(String),
241    /// Received after `Message::PackStart` when stream code is 3
242    ///
243    /// Fatal error message
244    PackError(String),
245}
246
247pub struct PktIter {
248    inner: Box<dyn Read + Send>,
249    is_data: bool,
250}
251
252impl PktIter {
253    pub fn new(reader: impl Read + Send + 'static) -> Self {
254        Self {
255            inner: Box::new(reader),
256            is_data: false,
257        }
258    }
259}
260
261impl Iterator for PktIter {
262    type Item = Message;
263
264    fn next(&mut self) -> Option<Self::Item> {
265        let (mut data, len) = io::read_pktline(&mut self.inner).unwrap();
266        if len == 0 && data.len() == 0 {
267            None
268        } else if len > 0 && self.is_data {
269            match data[0] {
270                1 => {
271                    // pack data
272                    data.remove(0);
273                    Some(Message::PackData(data))
274                }
275                2 => {
276                    // progress message
277                    Some(Message::PackProgress(String::from_utf8_lossy(&data[1..]).trim().to_owned()))
278                }
279                3 => {
280                    // fatal error
281                    Some(Message::PackError(String::from_utf8_lossy(&data[1..]).trim().to_owned()))
282                }
283                _ => unreachable!(),
284            }
285        } else if data == b"packfile\n" {
286            self.is_data = true;
287            Some(Message::PackStart)
288        } else {
289            Some(match len {
290                0 => Message::Flush,
291                1 => Message::Delimeter,
292                2 => Message::ResponseEnd,
293                _ => Message::Normal(data),
294            })
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use crate::{Client, Pack};
302    use crate::client::Message::*;
303    use std::io::Cursor;
304    use crate::client::RequestBuilder;
305
306    #[test]
307    fn test_handshake() {
308        let v: Vec<_> = Client::new("https://github.com/project-anni/repo.git").handshake().unwrap().collect();
309        let (l, r) = v.split_at(3);
310        let (agent, r) = r.split_at(1);
311        assert_eq!(l, vec![
312            Normal(b"# service=git-upload-pack\n".to_vec()),
313            Flush,
314            Normal(b"version 2\n".to_vec()),
315        ]);
316        match &agent[0] {
317            Normal(n) => assert!(n.starts_with(b"agent=git/github-")),
318            _ => panic!("invalid agent message type"),
319        }
320        assert_eq!(r, vec![
321            Normal(b"ls-refs\n".to_vec()),
322            Normal(b"fetch=shallow filter\n".to_vec()),
323            Normal(b"server-option\n".to_vec()),
324            Normal(b"object-format=sha1\n".to_vec()),
325            Flush,
326        ]);
327    }
328
329    #[test]
330    fn test_ls_ref() {
331        let hash = Client::new("https://github.com/project-anni/anni-fetch.git")
332            .ls_ref("refs/tags")
333            .unwrap();
334        assert_eq!(hash, "9192b5e5f2941fd76aa5a08043dc8aa6a31831a2");
335    }
336
337    #[test]
338    fn test_fetch_iter() {
339        let client = Client::new("https://github.com/project-anni/repo.git");
340        let iter = client.request(
341            RequestBuilder::new(true)
342                .command("fetch")
343                .argument("thin-pack")
344                .argument("ofs-delta")
345                .argument("deepen 1")
346                .want(&client.ls_ref("HEAD").expect("failed to get sha1 of HEAD"))
347                .argument("done")
348                .build()
349        ).unwrap();
350        let mut pack = Vec::new();
351        for msg in iter {
352            match msg {
353                PackData(mut d) => pack.append(&mut d),
354                _ => {}
355            }
356        }
357        let mut cursor = Cursor::new(pack);
358        Pack::from_reader(&mut cursor).expect("invalid pack file");
359    }
360}