Skip to main content

sley_protocol/
sideband.rs

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