Skip to main content

sley_protocol/
sideband.rs

1use sley_core::{GitError, Result};
2use std::io::{Read, Write};
3
4use crate::pktline::{
5    PKT_LINE_MAX_PAYLOAD_LEN, PktLineFrame, line_from_str, parse_protocol_v2_line_text,
6    pkt_line_header, read_pkt_line_frames_until_flush, write_pkt_line_payload,
7};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum SideBandChannel {
11    Data,
12    Progress,
13    Fatal,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct SideBandPacket {
18    pub channel: SideBandChannel,
19    pub data: Vec<u8>,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Default)]
23pub struct SideBandDemux {
24    pub data: Vec<u8>,
25    pub progress: Vec<Vec<u8>>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Default)]
29pub struct UploadArchiveRequest {
30    pub arguments: Vec<String>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum UploadArchiveResponse {
35    Ack { sideband: Vec<SideBandPacket> },
36    Nack { message: String },
37}
38pub fn parse_sideband_packet(payload: &[u8]) -> Result<SideBandPacket> {
39    let Some((&channel, data)) = payload.split_first() else {
40        return Err(GitError::InvalidFormat("sideband packet is empty".into()));
41    };
42    let channel = match channel {
43        1 => SideBandChannel::Data,
44        2 => SideBandChannel::Progress,
45        3 => SideBandChannel::Fatal,
46        other => {
47            return Err(GitError::InvalidFormat(format!(
48                "invalid sideband channel {other}"
49            )));
50        }
51    };
52    Ok(SideBandPacket {
53        channel,
54        data: data.to_vec(),
55    })
56}
57
58pub fn encode_sideband_packet(packet: &SideBandPacket) -> Result<Vec<u8>> {
59    let mut out = Vec::with_capacity(packet.data.len() + 1);
60    out.push(match packet.channel {
61        SideBandChannel::Data => 1,
62        SideBandChannel::Progress => 2,
63        SideBandChannel::Fatal => 3,
64    });
65    out.extend_from_slice(&packet.data);
66    if out.len() > PKT_LINE_MAX_PAYLOAD_LEN {
67        return Err(GitError::InvalidFormat(format!(
68            "sideband packet exceeds {PKT_LINE_MAX_PAYLOAD_LEN} bytes"
69        )));
70    }
71    Ok(out)
72}
73
74pub fn write_sideband_packet(writer: &mut impl Write, packet: &SideBandPacket) -> Result<()> {
75    write_sideband_payload(writer, packet.channel, &packet.data)
76}
77
78pub(crate) fn write_sideband_payload(
79    writer: &mut impl Write,
80    channel: SideBandChannel,
81    data: &[u8],
82) -> Result<()> {
83    let payload_len = data
84        .len()
85        .checked_add(1)
86        .ok_or_else(|| GitError::InvalidFormat("sideband packet length overflow".into()))?;
87    if payload_len > PKT_LINE_MAX_PAYLOAD_LEN {
88        return Err(GitError::InvalidFormat(format!(
89            "sideband packet exceeds {PKT_LINE_MAX_PAYLOAD_LEN} bytes"
90        )));
91    }
92    writer.write_all(&pkt_line_header(payload_len + 4))?;
93    writer.write_all(&[match channel {
94        SideBandChannel::Data => 1,
95        SideBandChannel::Progress => 2,
96        SideBandChannel::Fatal => 3,
97    }])?;
98    writer.write_all(data)?;
99    Ok(())
100}
101
102pub fn parse_sideband_packets(payloads: &[Vec<u8>]) -> Result<Vec<SideBandPacket>> {
103    payloads
104        .iter()
105        .map(|payload| parse_sideband_packet(payload))
106        .collect()
107}
108
109pub fn encode_sideband_packets(packets: &[SideBandPacket]) -> Result<Vec<Vec<u8>>> {
110    packets.iter().map(encode_sideband_packet).collect()
111}
112
113pub fn parse_sideband_stream(frames: &[PktLineFrame]) -> Result<Vec<SideBandPacket>> {
114    let mut packets = Vec::new();
115    let mut saw_flush = false;
116    for (idx, frame) in frames.iter().enumerate() {
117        match frame {
118            PktLineFrame::Data(payload) if !saw_flush => {
119                packets.push(parse_sideband_packet(payload)?);
120            }
121            PktLineFrame::Data(_) => {
122                return Err(GitError::InvalidFormat(
123                    "sideband stream has data after flush".into(),
124                ));
125            }
126            PktLineFrame::Flush => {
127                saw_flush = true;
128                if idx + 1 != frames.len() {
129                    return Err(GitError::InvalidFormat(
130                        "sideband stream has frames after flush".into(),
131                    ));
132                }
133            }
134            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
135                return Err(GitError::InvalidFormat(
136                    "sideband stream contains a non-flush control packet".into(),
137                ));
138            }
139        }
140    }
141    if !saw_flush {
142        return Err(GitError::InvalidFormat(
143            "sideband stream missing flush".into(),
144        ));
145    }
146    Ok(packets)
147}
148
149pub fn encode_sideband_stream(packets: &[SideBandPacket]) -> Result<Vec<PktLineFrame>> {
150    let mut frames = Vec::new();
151    for packet in packets {
152        frames.push(PktLineFrame::data(encode_sideband_packet(packet)?)?);
153    }
154    frames.push(PktLineFrame::Flush);
155    Ok(frames)
156}
157
158pub fn read_sideband_stream(reader: &mut impl Read) -> Result<Vec<SideBandPacket>> {
159    let frames = read_pkt_line_frames_until_flush(reader)?;
160    parse_sideband_stream(&frames)
161}
162
163pub fn write_sideband_stream(writer: &mut impl Write, packets: &[SideBandPacket]) -> Result<()> {
164    for packet in packets {
165        write_sideband_packet(writer, packet)?;
166    }
167    writer.write_all(b"0000")?;
168    Ok(())
169}
170
171pub fn demux_sideband_packets(packets: &[SideBandPacket]) -> Result<SideBandDemux> {
172    let mut out = SideBandDemux::default();
173    for packet in packets {
174        match packet.channel {
175            SideBandChannel::Data => out.data.extend_from_slice(&packet.data),
176            SideBandChannel::Progress => out.progress.push(packet.data.clone()),
177            SideBandChannel::Fatal => {
178                let message = String::from_utf8_lossy(&packet.data).into_owned();
179                return Err(GitError::InvalidFormat(format!(
180                    "sideband fatal: {message}"
181                )));
182            }
183        }
184    }
185    Ok(out)
186}
187
188pub fn parse_and_demux_sideband_packets(payloads: &[Vec<u8>]) -> Result<SideBandDemux> {
189    let packets = parse_sideband_packets(payloads)?;
190    demux_sideband_packets(&packets)
191}
192
193pub fn demux_sideband_stream(frames: &[PktLineFrame]) -> Result<SideBandDemux> {
194    let packets = parse_sideband_stream(frames)?;
195    demux_sideband_packets(&packets)
196}
197
198pub fn read_and_demux_sideband_stream(reader: &mut impl Read) -> Result<SideBandDemux> {
199    let packets = read_sideband_stream(reader)?;
200    demux_sideband_packets(&packets)
201}
202
203pub fn parse_upload_archive_request(frames: &[PktLineFrame]) -> Result<UploadArchiveRequest> {
204    let mut request = UploadArchiveRequest::default();
205    let mut saw_flush = false;
206    for (idx, frame) in frames.iter().enumerate() {
207        match frame {
208            PktLineFrame::Data(payload) if !saw_flush => {
209                let text = parse_protocol_v2_line_text("upload-archive request argument", payload)?;
210                let argument = text.strip_prefix("argument ").ok_or_else(|| {
211                    GitError::InvalidFormat("upload-archive request line must be argument".into())
212                })?;
213                validate_upload_archive_argument(argument)?;
214                request.arguments.push(argument.to_string());
215            }
216            PktLineFrame::Data(_) => {
217                return Err(GitError::InvalidFormat(
218                    "upload-archive request has data after flush".into(),
219                ));
220            }
221            PktLineFrame::Flush => {
222                saw_flush = true;
223                if idx + 1 != frames.len() {
224                    return Err(GitError::InvalidFormat(
225                        "upload-archive request has frames after flush".into(),
226                    ));
227                }
228            }
229            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
230                return Err(GitError::InvalidFormat(
231                    "upload-archive request contains a non-flush control packet".into(),
232                ));
233            }
234        }
235    }
236    if !saw_flush {
237        return Err(GitError::InvalidFormat(
238            "upload-archive request missing flush".into(),
239        ));
240    }
241    if request.arguments.is_empty() {
242        return Err(GitError::InvalidFormat(
243            "upload-archive request is missing arguments".into(),
244        ));
245    }
246    Ok(request)
247}
248
249pub fn encode_upload_archive_request(request: &UploadArchiveRequest) -> Result<Vec<PktLineFrame>> {
250    if request.arguments.is_empty() {
251        return Err(GitError::InvalidFormat(
252            "upload-archive request is missing arguments".into(),
253        ));
254    }
255    let mut frames = Vec::new();
256    for argument in &request.arguments {
257        validate_upload_archive_argument(argument)?;
258        frames.push(PktLineFrame::data(line_from_str(&format!(
259            "argument {argument}"
260        )))?);
261    }
262    frames.push(PktLineFrame::Flush);
263    Ok(frames)
264}
265
266pub fn read_upload_archive_request(reader: &mut impl Read) -> Result<UploadArchiveRequest> {
267    let frames = read_pkt_line_frames_until_flush(reader)?;
268    parse_upload_archive_request(&frames)
269}
270
271pub fn write_upload_archive_request(
272    writer: &mut impl Write,
273    request: &UploadArchiveRequest,
274) -> Result<()> {
275    if request.arguments.is_empty() {
276        return Err(GitError::InvalidFormat(
277            "upload-archive request is missing arguments".into(),
278        ));
279    }
280    for argument in &request.arguments {
281        validate_upload_archive_argument(argument)?;
282        write_pkt_line_payload(writer, &line_from_str(&format!("argument {argument}")))?;
283    }
284    writer.write_all(b"0000")?;
285    Ok(())
286}
287
288pub fn parse_upload_archive_response(frames: &[PktLineFrame]) -> Result<UploadArchiveResponse> {
289    let Some((first, rest)) = frames.split_first() else {
290        return Err(GitError::InvalidFormat(
291            "upload-archive response is empty".into(),
292        ));
293    };
294    let PktLineFrame::Data(payload) = first else {
295        return Err(GitError::InvalidFormat(
296            "upload-archive response must start with a data packet".into(),
297        ));
298    };
299    let text = parse_protocol_v2_line_text("upload-archive response status", payload)?;
300    if text == "ACK" {
301        return Ok(UploadArchiveResponse::Ack {
302            sideband: parse_sideband_stream(rest)?,
303        });
304    }
305    if let Some(message) = text.strip_prefix("NACK ") {
306        validate_upload_archive_status_message(message)?;
307        if !matches!(rest, [PktLineFrame::Flush]) {
308            return Err(GitError::InvalidFormat(
309                "upload-archive NACK response must end with flush".into(),
310            ));
311        }
312        return Ok(UploadArchiveResponse::Nack {
313            message: message.to_string(),
314        });
315    }
316    Err(GitError::InvalidFormat(format!(
317        "unsupported upload-archive response status {text}"
318    )))
319}
320
321pub fn encode_upload_archive_response(
322    response: &UploadArchiveResponse,
323) -> Result<Vec<PktLineFrame>> {
324    let mut frames = Vec::new();
325    match response {
326        UploadArchiveResponse::Ack { sideband } => {
327            frames.push(PktLineFrame::data(line_from_str("ACK"))?);
328            frames.extend(encode_sideband_stream(sideband)?);
329        }
330        UploadArchiveResponse::Nack { message } => {
331            validate_upload_archive_status_message(message)?;
332            frames.push(PktLineFrame::data(line_from_str(&format!(
333                "NACK {message}"
334            )))?);
335            frames.push(PktLineFrame::Flush);
336        }
337    }
338    Ok(frames)
339}
340
341pub fn read_upload_archive_response(reader: &mut impl Read) -> Result<UploadArchiveResponse> {
342    let frames = read_pkt_line_frames_until_flush(reader)?;
343    parse_upload_archive_response(&frames)
344}
345
346pub fn write_upload_archive_response(
347    writer: &mut impl Write,
348    response: &UploadArchiveResponse,
349) -> Result<()> {
350    match response {
351        UploadArchiveResponse::Ack { sideband } => {
352            write_pkt_line_payload(writer, b"ACK\n")?;
353            write_sideband_stream(writer, sideband)?;
354        }
355        UploadArchiveResponse::Nack { message } => {
356            validate_upload_archive_status_message(message)?;
357            write_pkt_line_payload(writer, &line_from_str(&format!("NACK {message}")))?;
358            writer.write_all(b"0000")?;
359        }
360    }
361    Ok(())
362}
363
364pub fn demux_upload_archive_response(response: &UploadArchiveResponse) -> Result<SideBandDemux> {
365    match response {
366        UploadArchiveResponse::Ack { sideband } => demux_sideband_packets(sideband),
367        UploadArchiveResponse::Nack { message } => Err(GitError::InvalidFormat(format!(
368            "upload-archive NACK: {message}"
369        ))),
370    }
371}
372fn validate_upload_archive_argument(value: &str) -> Result<()> {
373    if value.is_empty() {
374        return Err(GitError::InvalidFormat(
375            "upload-archive argument is empty".into(),
376        ));
377    }
378    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
379        return Err(GitError::InvalidFormat(
380            "upload-archive argument contains a delimiter byte".into(),
381        ));
382    }
383    Ok(())
384}
385
386fn validate_upload_archive_status_message(value: &str) -> Result<()> {
387    if value.is_empty() {
388        return Err(GitError::InvalidFormat(
389            "upload-archive status message is empty".into(),
390        ));
391    }
392    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
393        return Err(GitError::InvalidFormat(
394            "upload-archive status message contains a delimiter byte".into(),
395        ));
396    }
397    Ok(())
398}