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}