packfile/
packet_line.rs

1use crate::{low_level::PackFile, Error};
2use bytes::{BufMut, BytesMut};
3use std::fmt::Write;
4
5/// The maximum length of a pkt-line's data component is 65516 bytes.
6/// Implementations MUST NOT send pkt-line whose length exceeds 65520
7/// (65516 bytes of payload + 4 bytes of length data).
8///
9/// <https://git-scm.com/docs/protocol-common#_pkt_line_format>
10const MAX_DATA_LEN: usize = 65516;
11
12/// A wrapper containing every possible type of message that can be sent to a Git client.
13pub enum PktLine<'a> {
14    /// General data sent to a client, generally a UTF-8 encoded string.
15    Data(&'a [u8]),
16    /// Similar to a data packet, but used during packfile sending to indicate this
17    /// packet is a block of data by appending a byte containing the u8 `1`.
18    SidebandData(PackFile<'a>),
19    /// Similar to a data packet, but used during packfile sending to indicate this
20    /// packet is a status message by appending a byte containing the u8 `2`.
21    SidebandMsg(&'a [u8]),
22    /// Indicates the end of a response.
23    Flush,
24    /// Separates sections of a response.
25    Delimiter,
26    /// Indicates the end of the response, allowing the client to send another request.
27    ResponseEnd,
28}
29
30impl PktLine<'_> {
31    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, buf), err))]
32    pub fn encode_to(&self, buf: &mut BytesMut) -> Result<(), Error> {
33        match self {
34            Self::Data(data) => {
35                for chunk in data.chunks(MAX_DATA_LEN) {
36                    write!(buf, "{:04x}", chunk.len() + 4)?;
37                    buf.extend_from_slice(chunk);
38                }
39            }
40            Self::SidebandData(packfile) => {
41                // split the buf off so the cost of counting the bytes to put in the
42                // data line prefix is just the cost of `unsplit` (an atomic decrement)
43                let mut data_buf = buf.split_off(buf.len());
44
45                packfile.encode_to(&mut data_buf)?;
46
47                // write into the buf not the data buf so it's at the start of the msg
48                if data_buf.len() + 5 <= MAX_DATA_LEN - 1 {
49                    write!(buf, "{:04x}", data_buf.len() + 5)?;
50                    buf.put_u8(1); // sideband, 1 = data
51                    buf.unsplit(data_buf);
52                } else {
53                    for chunk in data_buf.chunks(MAX_DATA_LEN - 1) {
54                        write!(buf, "{:04x}", chunk.len() + 5)?;
55                        buf.put_u8(1); // sideband, 1 = data
56                        buf.extend_from_slice(chunk);
57                    }
58                }
59            }
60            Self::SidebandMsg(msg) => {
61                for chunk in msg.chunks(MAX_DATA_LEN - 1) {
62                    write!(buf, "{:04x}", chunk.len() + 5)?;
63                    buf.put_u8(2); // sideband, 2 = msg
64                    buf.extend_from_slice(chunk);
65                }
66            }
67            Self::Flush => buf.extend_from_slice(b"0000"),
68            Self::Delimiter => buf.extend_from_slice(b"0001"),
69            Self::ResponseEnd => buf.extend_from_slice(b"0002"),
70        }
71
72        Ok(())
73    }
74}
75
76impl<'a> From<&'a str> for PktLine<'a> {
77    fn from(val: &'a str) -> Self {
78        PktLine::Data(val.as_bytes())
79    }
80}
81
82#[cfg(test)]
83mod test {
84    use crate::packet_line::MAX_DATA_LEN;
85    use bytes::BytesMut;
86
87    #[test]
88    fn test_pkt_line() {
89        let mut buffer = BytesMut::new();
90        super::PktLine::from("agent=git/2.32.0\n")
91            .encode_to(&mut buffer)
92            .unwrap();
93        assert_eq!(buffer.as_ref(), b"0015agent=git/2.32.0\n");
94    }
95
96    #[test]
97    fn test_large_pkt_line() {
98        let mut buffer = BytesMut::new();
99        super::PktLine::from("a".repeat(70000).as_str())
100            .encode_to(&mut buffer)
101            .unwrap();
102        assert_eq!(
103            buffer.len(),
104            70008,
105            "should be two chunks each with a 4-byte len header"
106        );
107
108        // chunk 1
109        assert_eq!(
110            std::str::from_utf8(&buffer[..4]).unwrap(),
111            format!("{:04x}", 4 + MAX_DATA_LEN)
112        );
113        assert!(
114            &buffer[4..4 + MAX_DATA_LEN]
115                .iter()
116                .all(|b| char::from(*b) == 'a'),
117            "data should be all 'a's"
118        );
119
120        // chunk 2
121        assert_eq!(
122            std::str::from_utf8(&buffer[4 + MAX_DATA_LEN..][..4]).unwrap(),
123            format!("{:04x}", 4 + (70000 - MAX_DATA_LEN))
124        );
125        assert!(
126            &buffer[4 + MAX_DATA_LEN + 4..]
127                .iter()
128                .all(|b| char::from(*b) == 'a'),
129            "data should be all 'a's"
130        );
131    }
132}