Skip to main content

git_xcrypt/git/
pktline.rs

1//! The pkt-line framing git's long-running filter protocol speaks.
2//!
3//! A packet is four hexadecimal digits of length — counting those four bytes —
4//! followed by the payload. `0000` is a flush, which ends a list. Payloads are
5//! arbitrary bytes, so nothing here may become a `String`.
6
7use std::io::{Read, Write};
8
9use crate::{Error, Result};
10
11/// Largest payload one packet can carry: git's limit minus the length prefix.
12pub const MAX_PAYLOAD: usize = 65516;
13
14/// One item read from the stream.
15#[derive(Debug, PartialEq, Eq)]
16pub enum Packet {
17    /// A payload, without its length prefix.
18    Data(Vec<u8>),
19    /// The `0000` that ends a list.
20    Flush,
21}
22
23/// Reads one packet.
24///
25/// # Errors
26///
27/// [`Error::Io`] on a read failure, [`Error::Format`] on a malformed length.
28pub fn read_packet(input: &mut impl Read) -> Result<Packet> {
29    let mut length = [0u8; 4];
30    input.read_exact(&mut length)?;
31
32    // Four hexadecimal digits and nothing else. `from_str_radix` alone would
33    // accept `+abc`, and this is the one parser standing between a malformed
34    // stream and the rest of the filter.
35    if !length.iter().all(u8::is_ascii_hexdigit) {
36        return Err(Error::Format(
37            "the filter protocol sent a non-hex packet length".into(),
38        ));
39    }
40    let text = std::str::from_utf8(&length)
41        .map_err(|_| Error::Format("the filter protocol sent a non-hex packet length".into()))?;
42    let length = usize::from_str_radix(text, 16)
43        .map_err(|_| Error::Format(format!("the filter protocol sent a bad length `{text}`")))?;
44
45    if length == 0 {
46        return Ok(Packet::Flush);
47    }
48    if length < 4 {
49        return Err(Error::Format(format!(
50            "the filter protocol sent an impossible packet length {length}"
51        )));
52    }
53
54    let mut payload = vec![0u8; length - 4];
55    input.read_exact(&mut payload)?;
56    Ok(Packet::Data(payload))
57}
58
59/// Writes one payload, splitting it across packets when it is too long.
60///
61/// # Errors
62///
63/// [`Error::Io`] on a write failure.
64pub fn write_data(output: &mut impl Write, payload: &[u8]) -> Result<()> {
65    for chunk in payload.chunks(MAX_PAYLOAD) {
66        write!(output, "{:04x}", chunk.len() + 4)?;
67        output.write_all(chunk)?;
68    }
69    Ok(())
70}
71
72/// Writes a flush and pushes everything out.
73///
74/// The flush is useless if it sits in a buffer: git is waiting for it before it
75/// will say anything else, so this is where a missing flush turns into a hang.
76///
77/// # Errors
78///
79/// [`Error::Io`] on a write failure.
80pub fn write_flush(output: &mut impl Write) -> Result<()> {
81    output.write_all(b"0000")?;
82    output.flush()?;
83    Ok(())
84}
85
86/// Reads packets until the next flush, returning their payloads.
87///
88/// # Errors
89///
90/// As [`read_packet`].
91pub fn read_until_flush(input: &mut impl Read) -> Result<Vec<Vec<u8>>> {
92    let mut items = Vec::new();
93    loop {
94        match read_packet(input)? {
95            Packet::Flush => return Ok(items),
96            Packet::Data(payload) => items.push(payload),
97        }
98    }
99}
100
101/// Reads packets until the next flush and concatenates them.
102///
103/// # Errors
104///
105/// As [`read_packet`].
106pub fn read_content(input: &mut impl Read) -> Result<Vec<u8>> {
107    let mut content = Vec::new();
108    loop {
109        match read_packet(input)? {
110            Packet::Flush => return Ok(content),
111            Packet::Data(payload) => content.extend_from_slice(&payload),
112        }
113    }
114}