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