Skip to main content

grit_lib/
pkt_line.rs

1//! Git pkt-line format helpers.
2//!
3//! Implements reading and writing of the pkt-line framing used in the
4//! Git wire protocol (both v0 and v2).
5
6use std::io::{self, Read, Write};
7
8/// Special packet type: flush packet (`0000`).
9pub const FLUSH: &str = "0000";
10/// Special packet type: delimiter packet (`0001`).
11pub const DELIM: &str = "0001";
12/// Special packet type: response-end packet (`0002`).
13pub const RESPONSE_END: &str = "0002";
14
15/// Write a single pkt-line to `w`. The data should not include a trailing
16/// newline; one is appended automatically.
17pub fn write_line(w: &mut impl Write, data: &str) -> io::Result<()> {
18    let len = 4 + data.len() + 1;
19    writeln!(w, "{len:04x}{data}")
20}
21
22/// Append a pkt-line encoding of `data` (with trailing newline) to `buf`.
23pub fn write_line_to_vec(buf: &mut Vec<u8>, data: &str) -> io::Result<()> {
24    let len = 4 + data.len() + 1;
25    let line = format!("{len:04x}{data}\n");
26    buf.extend_from_slice(line.as_bytes());
27    Ok(())
28}
29
30/// Write a flush packet (`0000`).
31pub fn write_flush(w: &mut impl Write) -> io::Result<()> {
32    write!(w, "0000")
33}
34
35/// Write one pkt-line with arbitrary bytes (no trailing newline added).
36pub fn write_packet_raw(w: &mut impl Write, payload: &[u8]) -> io::Result<()> {
37    let total = payload
38        .len()
39        .checked_add(4)
40        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "pkt-line payload too large"))?;
41    if total > 65520 {
42        return Err(io::Error::new(
43            io::ErrorKind::InvalidInput,
44            "pkt-line exceeds maximum size",
45        ));
46    }
47    write!(w, "{total:04x}")?;
48    w.write_all(payload)?;
49    Ok(())
50}
51
52/// Write a delimiter packet (`0001`).
53pub fn write_delim(w: &mut impl Write) -> io::Result<()> {
54    write!(w, "0001")
55}
56
57/// A single packet read from the wire.
58#[derive(Debug, PartialEq, Eq)]
59pub enum Packet {
60    /// A data line (content without trailing newline).
61    Data(String),
62    /// Flush packet (`0000`).
63    Flush,
64    /// Delimiter packet (`0001`).
65    Delim,
66    /// Response-end packet (`0002`).
67    ResponseEnd,
68}
69
70/// Read one pkt-line from `r`. Returns `None` at EOF.
71pub fn read_packet(r: &mut impl Read) -> io::Result<Option<Packet>> {
72    let mut len_buf = [0u8; 4];
73    match r.read_exact(&mut len_buf) {
74        Ok(()) => {}
75        Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
76        Err(e) => return Err(e),
77    }
78    let len_str =
79        std::str::from_utf8(&len_buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
80    let len = usize::from_str_radix(len_str, 16)
81        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
82
83    match len {
84        0 => Ok(Some(Packet::Flush)),
85        1 => Ok(Some(Packet::Delim)),
86        2 => Ok(Some(Packet::ResponseEnd)),
87        n if n <= 4 => Err(io::Error::new(
88            io::ErrorKind::InvalidData,
89            format!("invalid pkt-line length: {n}"),
90        )),
91        n => {
92            let payload_len = n - 4;
93            let mut buf = vec![0u8; payload_len];
94            r.read_exact(&mut buf)?;
95            // Smart HTTP / CGI paths may emit non-UTF-8 in diagnostic pkt-lines; Git tolerates
96            // lossy decoding. NUL remains valid and is preserved.
97            let s = String::from_utf8_lossy(&buf).into_owned();
98            Ok(Some(Packet::Data(
99                s.strip_suffix('\n').unwrap_or(&s).to_owned(),
100            )))
101        }
102    }
103}
104
105/// Read packets from `r` until a flush or delimiter packet, or EOF.
106///
107/// Returns the collected data lines and the terminator (`Flush`, `Delim`, or `None`).
108pub fn read_until_flush_or_delim(r: &mut impl Read) -> io::Result<(Vec<String>, Option<Packet>)> {
109    let mut lines = Vec::new();
110    loop {
111        match read_packet(r)? {
112            None => return Ok((lines, None)),
113            Some(Packet::Flush) => return Ok((lines, Some(Packet::Flush))),
114            Some(Packet::Delim) => return Ok((lines, Some(Packet::Delim))),
115            Some(Packet::ResponseEnd) => return Ok((lines, Some(Packet::ResponseEnd))),
116            Some(Packet::Data(s)) => lines.push(s),
117        }
118    }
119}
120
121/// Read data pkt-lines until a flush packet, matching Git's command argument sections.
122///
123/// A delimiter or response-end packet is treated as a protocol violation and reported with
124/// `err_not_flush` (for example: `"expected flush after ls-refs arguments"`).
125pub fn read_data_lines_until_flush(
126    r: &mut impl Read,
127    err_not_flush: &str,
128) -> io::Result<Vec<String>> {
129    let mut lines = Vec::new();
130    loop {
131        match read_packet(r)? {
132            None => {
133                return Err(io::Error::new(
134                    io::ErrorKind::UnexpectedEof,
135                    err_not_flush.to_string(),
136                ));
137            }
138            Some(Packet::Flush) => return Ok(lines),
139            Some(Packet::Delim) | Some(Packet::ResponseEnd) => {
140                return Err(io::Error::new(
141                    io::ErrorKind::InvalidData,
142                    err_not_flush.to_string(),
143                ));
144            }
145            Some(Packet::Data(s)) => lines.push(s),
146        }
147    }
148}
149
150/// Write one sideband pkt-line on `band`.
151pub fn write_sideband_packet(w: &mut impl Write, band: u8, payload: &[u8]) -> io::Result<()> {
152    let len = 4 + 1 + payload.len();
153    write!(w, "{len:04x}")?;
154    w.write_all(&[band])?;
155    w.write_all(payload)?;
156    Ok(())
157}
158
159/// Write payload on sideband channel 1 using the same 64k chunking as `git upload-pack`.
160pub fn write_sideband_channel1_64k(w: &mut impl Write, payload: &[u8]) -> io::Result<()> {
161    const MAX_PAYLOAD: usize = 65515;
162    for chunk in payload.chunks(MAX_PAYLOAD) {
163        let len = 4 + 1 + chunk.len();
164        write!(w, "{len:04x}")?;
165        w.write_all(&[1u8])?;
166        w.write_all(chunk)?;
167    }
168    Ok(())
169}
170
171/// Parse a 4-byte hexadecimal pkt-line length prefix.
172pub fn parse_hex_len(prefix: &[u8]) -> io::Result<usize> {
173    let s = std::str::from_utf8(prefix)
174        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{e}")))?;
175    usize::from_str_radix(s, 16)
176        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("invalid length: {e}")))
177}
178
179/// Decode a sideband pkt-line stream into the raw primary (band 1) payload.
180pub fn decode_sideband_primary(mut input: &[u8]) -> io::Result<Vec<u8>> {
181    let mut out = Vec::new();
182    while !input.is_empty() {
183        if input.len() < 4 {
184            break;
185        }
186        let len = parse_hex_len(&input[..4])?;
187        input = &input[4..];
188        if len == 0 {
189            break;
190        }
191        if len <= 4 || input.len() < len - 4 {
192            return Err(io::Error::new(
193                io::ErrorKind::InvalidData,
194                "truncated sideband packet",
195            ));
196        }
197        let payload_len = len - 4;
198        let payload = &input[..payload_len];
199        input = &input[payload_len..];
200        if payload.is_empty() {
201            continue;
202        }
203        let band = payload[0];
204        let data = &payload[1..];
205        if band == 1 {
206            out.extend_from_slice(data);
207        }
208    }
209    Ok(out)
210}