hashtree_git/
protocol.rs

1//! Git protocol implementation (pkt-line format)
2//!
3//! Git uses "pkt-line" framing: 4 hex digits length prefix followed by data.
4//! Special values: "0000" (flush), "0001" (delimiter), "0002" (response-end)
5
6use crate::{Error, Result};
7
8/// Flush packet (marks end of message)
9pub const FLUSH_PKT: &[u8] = b"0000";
10/// Delimiter packet (separates sections)
11pub const DELIM_PKT: &[u8] = b"0001";
12/// Response end packet
13pub const RESPONSE_END_PKT: &[u8] = b"0002";
14
15/// Maximum pkt-line data size (65516 bytes payload + 4 length + 1 newline)
16pub const MAX_PKT_LINE: usize = 65520;
17
18/// Write a pkt-line
19pub fn pkt_line(data: &[u8]) -> Vec<u8> {
20    let len = data.len() + 4; // Include the 4-byte length prefix
21    let mut pkt = format!("{:04x}", len).into_bytes();
22    pkt.extend_from_slice(data);
23    pkt
24}
25
26/// Write a pkt-line with newline suffix
27pub fn pkt_line_with_newline(data: &str) -> Vec<u8> {
28    let line = format!("{}\n", data);
29    pkt_line(line.as_bytes())
30}
31
32/// Parse pkt-lines from a buffer
33pub struct PktLineReader<'a> {
34    data: &'a [u8],
35    pos: usize,
36}
37
38impl<'a> PktLineReader<'a> {
39    pub fn new(data: &'a [u8]) -> Self {
40        Self { data, pos: 0 }
41    }
42
43    /// Read the next pkt-line
44    pub fn read(&mut self) -> Result<Option<PktLine<'a>>> {
45        if self.pos + 4 > self.data.len() {
46            return Ok(None);
47        }
48
49        let len_hex = std::str::from_utf8(&self.data[self.pos..self.pos + 4])
50            .map_err(|_| Error::ProtocolError("invalid pkt-line length".into()))?;
51
52        // Special packets
53        match len_hex {
54            "0000" => {
55                self.pos += 4;
56                return Ok(Some(PktLine::Flush));
57            }
58            "0001" => {
59                self.pos += 4;
60                return Ok(Some(PktLine::Delimiter));
61            }
62            "0002" => {
63                self.pos += 4;
64                return Ok(Some(PktLine::ResponseEnd));
65            }
66            _ => {}
67        }
68
69        let len = usize::from_str_radix(len_hex, 16)
70            .map_err(|_| Error::ProtocolError("invalid pkt-line length".into()))?;
71
72        if len < 4 {
73            return Err(Error::ProtocolError("pkt-line length too small".into()));
74        }
75
76        if len > MAX_PKT_LINE {
77            return Err(Error::ProtocolError("pkt-line too large".into()));
78        }
79
80        if self.pos + len > self.data.len() {
81            return Err(Error::ProtocolError("pkt-line truncated".into()));
82        }
83
84        let payload = &self.data[self.pos + 4..self.pos + len];
85        self.pos += len;
86
87        Ok(Some(PktLine::Data(payload)))
88    }
89
90    /// Read all remaining pkt-lines until flush
91    pub fn read_until_flush(&mut self) -> Result<Vec<&'a [u8]>> {
92        let mut lines = Vec::new();
93        loop {
94            match self.read()? {
95                Some(PktLine::Flush) | None => break,
96                Some(PktLine::Data(data)) => lines.push(data),
97                Some(PktLine::Delimiter) => continue,
98                Some(PktLine::ResponseEnd) => break,
99            }
100        }
101        Ok(lines)
102    }
103
104    /// Remaining bytes
105    pub fn remaining(&self) -> &'a [u8] {
106        &self.data[self.pos..]
107    }
108}
109
110/// A pkt-line entry
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub enum PktLine<'a> {
113    Flush,
114    Delimiter,
115    ResponseEnd,
116    Data(&'a [u8]),
117}
118
119/// Build pkt-line responses
120pub struct PktLineWriter {
121    buffer: Vec<u8>,
122}
123
124impl PktLineWriter {
125    pub fn new() -> Self {
126        Self { buffer: Vec::new() }
127    }
128
129    pub fn write(&mut self, data: &[u8]) {
130        self.buffer.extend_from_slice(&pkt_line(data));
131    }
132
133    pub fn write_str(&mut self, s: &str) {
134        self.buffer.extend_from_slice(&pkt_line_with_newline(s));
135    }
136
137    pub fn flush(&mut self) {
138        self.buffer.extend_from_slice(FLUSH_PKT);
139    }
140
141    pub fn delimiter(&mut self) {
142        self.buffer.extend_from_slice(DELIM_PKT);
143    }
144
145    pub fn response_end(&mut self) {
146        self.buffer.extend_from_slice(RESPONSE_END_PKT);
147    }
148
149    pub fn write_raw(&mut self, data: &[u8]) {
150        self.buffer.extend_from_slice(data);
151    }
152
153    pub fn into_bytes(self) -> Vec<u8> {
154        self.buffer
155    }
156
157    pub fn as_bytes(&self) -> &[u8] {
158        &self.buffer
159    }
160}
161
162impl Default for PktLineWriter {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168/// Parse capability string from ref advertisement
169pub fn parse_capabilities(caps_str: &str) -> Vec<String> {
170    caps_str.split(' ')
171        .filter(|s| !s.is_empty())
172        .map(|s| s.to_string())
173        .collect()
174}
175
176/// Server capabilities for upload-pack
177pub const UPLOAD_PACK_CAPABILITIES: &[&str] = &[
178    "multi_ack",
179    "multi_ack_detailed",
180    "side-band-64k",
181    "thin-pack",
182    "ofs-delta",
183    "shallow",
184    "no-progress",
185    "include-tag",
186    "allow-tip-sha1-in-want",
187    "allow-reachable-sha1-in-want",
188    "no-done",
189];
190
191/// Server capabilities for receive-pack
192pub const RECEIVE_PACK_CAPABILITIES: &[&str] = &[
193    "report-status",
194    "delete-refs",
195    "side-band-64k",
196    "ofs-delta",
197    "atomic",
198];
199
200/// Format capabilities as string
201pub fn format_capabilities(caps: &[&str]) -> String {
202    caps.join(" ")
203}
204
205/// Side-band channel IDs
206pub mod sideband {
207    pub const DATA: u8 = 1;
208    pub const PROGRESS: u8 = 2;
209    pub const ERROR: u8 = 3;
210}
211
212/// Write data to a side-band channel
213pub fn sideband_pkt(channel: u8, data: &[u8]) -> Vec<u8> {
214    let mut payload = vec![channel];
215    payload.extend_from_slice(data);
216    pkt_line(&payload)
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_pkt_line() {
225        let pkt = pkt_line(b"hello");
226        assert_eq!(pkt, b"0009hello");
227    }
228
229    #[test]
230    fn test_pkt_line_with_newline() {
231        let pkt = pkt_line_with_newline("hello");
232        assert_eq!(pkt, b"000ahello\n");
233    }
234
235    #[test]
236    fn test_reader() {
237        let data = b"0009hello0006ab0000";
238        let mut reader = PktLineReader::new(data);
239
240        assert_eq!(reader.read().unwrap(), Some(PktLine::Data(b"hello")));
241        assert_eq!(reader.read().unwrap(), Some(PktLine::Data(b"ab")));
242        assert_eq!(reader.read().unwrap(), Some(PktLine::Flush));
243        assert_eq!(reader.read().unwrap(), None);
244    }
245
246    #[test]
247    fn test_writer() {
248        let mut writer = PktLineWriter::new();
249        writer.write_str("hello");
250        writer.flush();
251
252        assert_eq!(writer.as_bytes(), b"000ahello\n0000");
253    }
254}