Skip to main content

gunnar_sendpack/
driver.rs

1//! The client half: pkt-line framing over a blocking stream, and the
2//! [`send_pack`] driver that runs one whole push.
3//!
4//! # The conversation, exactly
5//!
6//! ```text
7//! S→C   <old-oid> <refname>\0<capabilities>\n     ref advertisement, protocol v0
8//! S→C   <old-oid> <refname>\n                     …one line per ref
9//! S→C   0000
10//! C→S   <old> <new> <refname>\0<capabilities>\n   the command list
11//! C→S   <old> <new> <refname>\n                   …one line per ref being moved
12//! C→S   0000
13//! C→S   <push-option>\n … 0000                    only if `push-options` was negotiated
14//! C→S   PACK…                                     raw, NOT pkt-line framed
15//! S→C   unpack ok\n                               report-status
16//! S→C   ok refs/heads/main\n | ng refs/heads/x <why>\n
17//! S→C   0000
18//! ```
19//!
20//! Three details are where implementations go wrong, and each has a test:
21//!
22//! 1. **The packfile is not framed.** It follows the flush-pkt as raw bytes.
23//!    Framing it is a corrupt push with a checksum error and no clue pointing
24//!    at the framing layer.
25//! 2. **An all-deletes push sends no pack at all.** Sending an empty pack is
26//!    also legal, but sending one for a push that introduces no objects is a
27//!    lie about what the client did.
28//! 3. **With `side-band-64k`, the report is muxed inside band 1**, pkt-lines
29//!    nested inside pkt-lines, and the remote's hook output arrives on band 2.
30//!    Reading the report without demuxing yields a leading `\x01` on every line
31//!    and a parse that fails in a way that looks like a protocol error.
32
33use std::io::{Read, Write};
34
35use gix_hash::Kind;
36use gix_packetline::blocking_io::encode;
37use gix_packetline::decode::{self, Stream};
38use gix_packetline::{Channel, PacketLineRef};
39
40use crate::advertisement::{self, Advertisement};
41use crate::command::{self, PushCommand};
42use crate::error::{Error, Result};
43use crate::options::{self, SendPackOptions};
44use crate::report::{self, PushReport};
45use crate::transport::Transport;
46
47/// Pulls whole pkt-lines out of a byte stream, refilling as needed.
48///
49/// The one rule that matters: **a short buffer is *incomplete*, never
50/// malformed.** A packet split across two reads — including split inside its
51/// own four-byte header — is normal on any real socket, and treating it as an
52/// error makes a timing-dependent failure that will not reproduce.
53/// [`gix_packetline::decode::streaming`] says which of the two it is, and this
54/// reader exists to keep asking rather than to guess.
55struct PktReader<'a> {
56    inner: &'a mut dyn Read,
57    buffer: Vec<u8>,
58    consumed: usize,
59    eof: bool,
60}
61
62impl<'a> PktReader<'a> {
63    fn new(inner: &'a mut dyn Read) -> Self {
64        PktReader {
65            inner,
66            buffer: Vec::new(),
67            consumed: 0,
68            eof: false,
69        }
70    }
71
72    /// The next packet as an owned payload description, or `None` at a clean
73    /// end of stream.
74    fn next(&mut self) -> Result<Option<Line>> {
75        loop {
76            if self.consumed < self.buffer.len() {
77                match decode::streaming(&self.buffer[self.consumed..]) {
78                    Ok(Stream::Complete {
79                        line,
80                        bytes_consumed,
81                    }) => {
82                        let out = Line::from(line);
83                        self.consumed += bytes_consumed;
84                        // Reclaim once the read cursor has passed most of the
85                        // buffer, rather than on every packet: a report is a
86                        // handful of lines, but the same reader walks a
87                        // sideband-muxed stream that can be megabytes.
88                        if self.consumed > 64 * 1024 {
89                            self.buffer.drain(..self.consumed);
90                            self.consumed = 0;
91                        }
92                        return Ok(Some(out));
93                    }
94                    Ok(Stream::Incomplete { .. }) => {}
95                    Err(err) => return Err(Error::protocol(format!("malformed pkt-line: {err}"))),
96                }
97            }
98            if self.eof {
99                let pending = self.buffer.len() - self.consumed;
100                if pending > 0 {
101                    return Err(Error::protocol(format!(
102                        "stream ended mid-packet with {pending} bytes buffered"
103                    )));
104                }
105                return Ok(None);
106            }
107            let mut chunk = [0u8; 8192];
108            let n = self.inner.read(&mut chunk)?;
109            if n == 0 {
110                self.eof = true;
111            } else {
112                self.buffer.extend_from_slice(&chunk[..n]);
113            }
114        }
115    }
116
117    /// Every payload up to the next flush-pkt.
118    ///
119    /// A clean EOF before the flush is accepted and yields what arrived; some
120    /// servers close instead of flushing after an error, and turning that into
121    /// a protocol error would replace the remote's diagnosis with our own.
122    fn payloads_until_flush(&mut self) -> Result<Vec<Vec<u8>>> {
123        let mut out = Vec::new();
124        while let Some(line) = self.next()? {
125            match line {
126                Line::Flush => return Ok(out),
127                Line::Data(d) => out.push(d),
128                // v2-only control packets have no meaning in this v0
129                // conversation; carrying on past them would desync silently.
130                other => {
131                    return Err(Error::protocol(format!(
132                        "unexpected control packet in a receive-pack stream: {other:?}"
133                    )))
134                }
135            }
136        }
137        Ok(out)
138    }
139}
140
141/// One decoded packet, owned so the reader may refill its buffer.
142#[derive(Debug)]
143enum Line {
144    Data(Vec<u8>),
145    Flush,
146    Delimiter,
147    ResponseEnd,
148}
149
150impl From<PacketLineRef<'_>> for Line {
151    fn from(line: PacketLineRef<'_>) -> Self {
152        match line {
153            PacketLineRef::Data(d) => Line::Data(d.to_vec()),
154            PacketLineRef::Flush => Line::Flush,
155            PacketLineRef::Delimiter => Line::Delimiter,
156            PacketLineRef::ResponseEnd => Line::ResponseEnd,
157        }
158    }
159}
160
161/// Read `git-receive-pack`'s ref advertisement from `reader`.
162pub fn read_advertisement(reader: &mut dyn Read) -> Result<Advertisement> {
163    let payloads = PktReader::new(reader).payloads_until_flush()?;
164    advertisement::parse(&payloads)
165}
166
167/// Frame `lines` as text pkt-lines and terminate with a flush-pkt.
168///
169/// *Text* framing is what git uses here: the newline is the framer's, which is
170/// why no `lines` function in this crate emits one.
171pub fn frame_section(lines: &[Vec<u8>], out: &mut dyn Write) -> Result<()> {
172    for line in lines {
173        encode::text_to_write(line, &mut *out)?;
174    }
175    encode::flush_to_write(&mut *out)?;
176    Ok(())
177}
178
179/// The command list, framed and flush-terminated.
180pub fn encode_command_list(commands: &[PushCommand], caps: &[String]) -> Result<Vec<u8>> {
181    let mut out = Vec::new();
182    frame_section(&command::lines(commands, caps)?, &mut out)?;
183    Ok(out)
184}
185
186/// The push-options section, framed and flush-terminated.
187pub fn encode_push_options(options: &[String]) -> Result<Vec<u8>> {
188    let mut out = Vec::new();
189    frame_section(&command::push_option_lines(options)?, &mut out)?;
190    Ok(out)
191}
192
193/// Read the report from `reader`, demultiplexing sidebands when negotiated.
194pub fn read_report(reader: &mut dyn Read, side_band: bool) -> Result<PushReport> {
195    let mut pkt = PktReader::new(reader);
196    if !side_band {
197        let payloads = pkt.payloads_until_flush()?;
198        return report::parse(&payloads);
199    }
200
201    // With `side-band-64k` the report is pkt-lines *inside* band 1 of
202    // pkt-lines. Band 1 is accumulated whole and decoded afterwards rather than
203    // packet-by-packet, because a nested pkt-line may straddle two band-1
204    // packets and nothing on the wire says so.
205    let mut band1: Vec<u8> = Vec::new();
206    let mut progress: Vec<String> = Vec::new();
207    let mut errors: Vec<String> = Vec::new();
208    while let Some(line) = pkt.next()? {
209        match line {
210            Line::Flush => break,
211            Line::Data(d) => {
212                let Some((&band, rest)) = d.split_first() else {
213                    return Err(Error::protocol(
214                        "an empty sideband packet carries no band number",
215                    ));
216                };
217                match band {
218                    b if b == Channel::Data as u8 => band1.extend_from_slice(rest),
219                    b if b == Channel::Progress as u8 => {
220                        progress.push(String::from_utf8_lossy(rest).into_owned())
221                    }
222                    b if b == Channel::Error as u8 => {
223                        errors.push(String::from_utf8_lossy(rest).into_owned())
224                    }
225                    other => {
226                        return Err(Error::protocol(format!(
227                            "unknown sideband {other} in the push report"
228                        )))
229                    }
230                }
231            }
232            other => {
233                return Err(Error::protocol(format!(
234                    "unexpected control packet in the push report: {other:?}"
235                )))
236            }
237        }
238    }
239
240    let mut cursor = std::io::Cursor::new(band1);
241    let mut inner = PktReader::new(&mut cursor);
242    let lines = inner.payloads_until_flush()?;
243    let mut out = report::parse(&lines)?;
244    out.progress = progress;
245    out.remote_errors = errors;
246    Ok(out)
247}
248
249/// Run one complete send-pack over `transport`, given an advertisement already
250/// read from it.
251///
252/// `write_pack` streams the packfile straight onto the wire; it is `None` for
253/// an all-deletes push, which carries no pack. Splitting the advertisement out
254/// of this function is what lets the caller decide *what* to push after seeing
255/// what the remote has; that decision is the porcelain's, not the wire's.
256pub fn send_pack<T, P>(
257    transport: &mut T,
258    advertisement: &Advertisement,
259    commands: &[PushCommand],
260    local_hash_kind: Kind,
261    write_pack: Option<P>,
262    opts: &SendPackOptions,
263) -> Result<PushReport>
264where
265    T: Transport,
266    P: FnOnce(&mut dyn Write) -> Result<()>,
267{
268    let capabilities = options::negotiate(advertisement, commands, local_hash_kind, opts)?;
269    let side_band = capabilities.iter().any(|c| c == "side-band-64k");
270    let expects_report = capabilities
271        .iter()
272        .any(|c| c == "report-status" || c == "report-status-v2");
273
274    let all_deletes = commands.iter().all(PushCommand::is_delete);
275    if all_deletes && write_pack.is_some() {
276        return Err(Error::invalid(
277            "an all-deletes push introduces no objects and must not carry a pack",
278        ));
279    }
280    if !all_deletes && write_pack.is_none() {
281        return Err(Error::invalid(
282            "a push that creates or updates a ref must carry a pack, even an empty one",
283        ));
284    }
285
286    let commands_bytes = encode_command_list(commands, &capabilities)?;
287    let options_bytes = if opts.push_options.is_empty() {
288        Vec::new()
289    } else {
290        encode_push_options(&opts.push_options)?
291    };
292
293    {
294        let (_, writer) = transport.io();
295        writer.write_all(&commands_bytes)?;
296        if !options_bytes.is_empty() {
297            writer.write_all(&options_bytes)?;
298        }
299        // The packfile follows the flush-pkt RAW, not pkt-line framed.
300        if let Some(write_pack) = write_pack {
301            write_pack(writer)?;
302        }
303        writer.flush()?;
304    }
305
306    if !expects_report {
307        return Ok(PushReport {
308            unpack: "ok".to_string(),
309            ..PushReport::default()
310        });
311    }
312    let (reader, _) = transport.io();
313    let mut report = read_report(reader, side_band)?;
314    // The report is the remote's account of what it did; the command list is
315    // the only record of what it was asked to do. Reconciling here rather than
316    // leaving it to each caller is what makes "no line about this ref" a
317    // failure by construction — a porcelain that forgot to check would report
318    // a push that moved nothing as a success. See `PushReport::is_ok`.
319    let commanded: Vec<bstr::BString> = commands.iter().map(|c| c.name.clone()).collect();
320    report.reconcile(&commanded);
321    Ok(report)
322}