1use sley_core::{Capability, GitError, ObjectFormat, ObjectId, Result};
2use std::io::{Read, Write};
3
4use crate::pktline::{
5 PktLineFrame, ProtocolVersion, line, line_from_str, parse_oid_argument,
6 parse_protocol_v2_line_text, read_pkt_line_frame, read_pkt_line_frames_until_flush,
7 read_pkt_line_frames_until_response_end, trace_packet_read_payload, trim_trailing_lf,
8 validate_capability_name, validate_protocol_v2_line, validate_protocol_v2_token,
9 write_pkt_line_frame, write_pkt_line_payload,
10};
11use crate::sideband::{
12 SideBandChannel, SideBandDemux, SideBandPacket, encode_sideband_packet,
13 parse_and_demux_sideband_packets, parse_sideband_packet, write_sideband_payload,
14};
15use crate::v0::{RefAdvertisement, RefAdvertisementSet, TransportHandshake};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ProtocolV2CommandRequest {
19 pub command: String,
20 pub capabilities: Vec<Capability>,
21 pub arguments: Vec<Vec<u8>>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum ProtocolV2Request {
26 Command(ProtocolV2CommandRequest),
27 Done,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum ProtocolV2Command {
32 LsRefs(ProtocolV2LsRefsRequest),
33 Fetch(ProtocolV2FetchRequest),
34 ObjectInfo(ProtocolV2ObjectInfoRequest),
35 Unknown(ProtocolV2CommandRequest),
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum ProtocolV2SessionRequest {
40 Command(ProtocolV2Command),
41 Done,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Default)]
45pub struct ProtocolV2CommandOptions {
46 pub agent: Option<String>,
47 pub object_format: Option<ObjectFormat>,
48 pub server_options: Vec<String>,
49 pub extra: Vec<Capability>,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Default)]
53pub struct ProtocolV2FetchFeatures {
54 pub shallow: bool,
55 pub wait_for_done: bool,
56 pub filter: bool,
57 pub ref_in_want: bool,
58 pub sideband_all: bool,
59 pub packfile_uris: bool,
60 pub unknown: Vec<String>,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Default)]
64pub struct ProtocolV2LsRefsFeatures {
65 pub unborn: bool,
66 pub unknown: Vec<String>,
67}
68
69impl ProtocolV2CommandRequest {
70 pub fn new(command: impl Into<String>) -> Result<Self> {
71 let command = command.into();
72 validate_capability_name(&command)?;
73 Ok(Self {
74 command,
75 capabilities: Vec::new(),
76 arguments: Vec::new(),
77 })
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Default)]
82pub struct ProtocolV2LsRefsRequest {
83 pub peel: bool,
84 pub symrefs: bool,
85 pub unborn: bool,
86 pub ref_prefixes: Vec<String>,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct ProtocolV2LsRefsRef {
91 pub oid: ObjectId,
92 pub name: String,
93 pub peeled: Option<ObjectId>,
94 pub symref_target: Option<String>,
95 pub attributes: Vec<String>,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum ProtocolV2LsRefsRecord {
100 Ref(ProtocolV2LsRefsRef),
101 Unborn {
102 name: String,
103 symref_target: Option<String>,
104 attributes: Vec<String>,
105 },
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Default)]
109pub struct ProtocolV2FetchRequest {
110 pub wants: Vec<ObjectId>,
111 pub want_refs: Vec<String>,
112 pub haves: Vec<ObjectId>,
113 pub shallow: Vec<ObjectId>,
114 pub deepen: Option<u32>,
115 pub deepen_since: Option<u64>,
116 pub deepen_not: Vec<String>,
117 pub deepen_relative: bool,
118 pub filter: Option<String>,
119 pub packfile_uris: Option<String>,
120 pub thin_pack: bool,
121 pub no_progress: bool,
122 pub include_tag: bool,
123 pub ofs_delta: bool,
124 pub sideband_all: bool,
125 pub wait_for_done: bool,
126 pub done: bool,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub enum ProtocolV2FetchAcknowledgment {
131 Nak,
132 Ack(ObjectId),
133 Ready,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub enum ProtocolV2FetchShallowInfo {
138 Shallow(ObjectId),
139 Unshallow(ObjectId),
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct ProtocolV2FetchWantedRef {
144 pub oid: ObjectId,
145 pub name: String,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct ProtocolV2FetchPackfileUri {
150 pub pack_hash: ObjectId,
151 pub uri: String,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum ProtocolV2FetchResponseSection {
156 Acknowledgments(Vec<ProtocolV2FetchAcknowledgment>),
157 ShallowInfo(Vec<ProtocolV2FetchShallowInfo>),
158 WantedRefs(Vec<ProtocolV2FetchWantedRef>),
159 PackfileUris(Vec<ProtocolV2FetchPackfileUri>),
160 Packfile(Vec<Vec<u8>>),
161 Unknown { name: String, lines: Vec<Vec<u8>> },
162}
163
164#[derive(Debug, Clone, PartialEq, Eq, Default)]
165pub struct ProtocolV2FetchSidebandAllResponse {
166 pub sections: Vec<ProtocolV2FetchResponseSection>,
167 pub progress: Vec<Vec<u8>>,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Default)]
171pub struct ProtocolV2FetchResponseHeader {
172 pub sections: Vec<ProtocolV2FetchResponseSection>,
173 pub has_packfile: bool,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Default)]
183pub struct ProtocolV2FetchNegotiationResponse {
184 pub acknowledgments: Vec<ProtocolV2FetchAcknowledgment>,
185 pub has_following_sections: bool,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Default)]
189pub struct ProtocolV2ObjectInfoRequest {
190 pub size: bool,
191 pub oids: Vec<ObjectId>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct ProtocolV2ObjectInfoRecord {
196 pub oid: ObjectId,
197 pub size: u64,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Default)]
201pub struct ProtocolV2ObjectInfoResponse {
202 pub size: bool,
203 pub records: Vec<ProtocolV2ObjectInfoRecord>,
204}
205
206impl ProtocolV2LsRefsRequest {
207 pub fn from_command_request(request: &ProtocolV2CommandRequest) -> Result<Self> {
208 if request.command != "ls-refs" {
209 return Err(GitError::InvalidFormat(format!(
210 "expected ls-refs command, got {}",
211 request.command
212 )));
213 }
214 let mut out = Self::default();
215 for argument in &request.arguments {
216 let text = std::str::from_utf8(argument)
217 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
218 match text {
219 "peel" => out.peel = true,
220 "symrefs" => out.symrefs = true,
221 "unborn" => out.unborn = true,
222 value if value.starts_with("ref-prefix ") => {
223 let prefix = value
224 .strip_prefix("ref-prefix ")
225 .ok_or_else(|| GitError::InvalidFormat("invalid ref-prefix".into()))?;
226 validate_protocol_v2_token("ls-refs ref-prefix", prefix)?;
227 out.ref_prefixes.push(prefix.to_string());
228 }
229 other => {
230 return Err(GitError::InvalidFormat(format!(
231 "unsupported ls-refs argument {other}"
232 )));
233 }
234 }
235 }
236 Ok(out)
237 }
238
239 pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
240 let mut request = ProtocolV2CommandRequest::new("ls-refs")?;
241 if self.peel {
242 request.arguments.push(b"peel".to_vec());
243 }
244 if self.symrefs {
245 request.arguments.push(b"symrefs".to_vec());
246 }
247 if self.unborn {
248 request.arguments.push(b"unborn".to_vec());
249 }
250 for prefix in &self.ref_prefixes {
251 validate_protocol_v2_token("ls-refs ref-prefix", prefix)?;
252 request
253 .arguments
254 .push(format!("ref-prefix {prefix}").into_bytes());
255 }
256 Ok(request)
257 }
258}
259
260impl ProtocolV2FetchRequest {
261 pub fn from_command_request(
262 format: ObjectFormat,
263 request: &ProtocolV2CommandRequest,
264 ) -> Result<Self> {
265 if request.command != "fetch" {
266 return Err(GitError::InvalidFormat(format!(
267 "expected fetch command, got {}",
268 request.command
269 )));
270 }
271 let mut out = Self::default();
272 for argument in &request.arguments {
273 let text = std::str::from_utf8(argument)
274 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
275 match text {
276 "thin-pack" => out.thin_pack = true,
277 "no-progress" => out.no_progress = true,
278 "include-tag" => out.include_tag = true,
279 "ofs-delta" => out.ofs_delta = true,
280 "sideband-all" => out.sideband_all = true,
281 "wait-for-done" => out.wait_for_done = true,
282 "deepen-relative" => out.deepen_relative = true,
283 "done" => out.done = true,
284 value if value.starts_with("want ") => {
285 out.wants
286 .push(parse_oid_argument(format, "fetch want", value, "want ")?);
287 }
288 value if value.starts_with("want-ref ") => {
289 let name = value
290 .strip_prefix("want-ref ")
291 .ok_or_else(|| GitError::InvalidFormat("invalid fetch want-ref".into()))?;
292 validate_protocol_v2_token("fetch want-ref", name)?;
293 out.want_refs.push(name.to_string());
294 }
295 value if value.starts_with("have ") => {
296 out.haves
297 .push(parse_oid_argument(format, "fetch have", value, "have ")?);
298 }
299 value if value.starts_with("shallow ") => {
300 out.shallow.push(parse_oid_argument(
301 format,
302 "fetch shallow",
303 value,
304 "shallow ",
305 )?);
306 }
307 value if value.starts_with("deepen ") => {
308 if out.deepen.is_some() {
309 return Err(GitError::InvalidFormat(
310 "fetch request has duplicate deepen".into(),
311 ));
312 }
313 out.deepen = Some(parse_u32_argument("fetch deepen", value, "deepen ")?);
314 }
315 value if value.starts_with("deepen-since ") => {
316 if out.deepen_since.is_some() {
317 return Err(GitError::InvalidFormat(
318 "fetch request has duplicate deepen-since".into(),
319 ));
320 }
321 out.deepen_since = Some(parse_u64_argument(
322 "fetch deepen-since",
323 value,
324 "deepen-since ",
325 )?);
326 }
327 value if value.starts_with("deepen-not ") => {
328 let name = value.strip_prefix("deepen-not ").ok_or_else(|| {
329 GitError::InvalidFormat("invalid fetch deepen-not".into())
330 })?;
331 validate_protocol_v2_token("fetch deepen-not", name)?;
332 out.deepen_not.push(name.to_string());
333 }
334 value if value.starts_with("filter ") => {
335 if out.filter.is_some() {
336 return Err(GitError::InvalidFormat(
337 "fetch request has duplicate filter".into(),
338 ));
339 }
340 let filter = value
341 .strip_prefix("filter ")
342 .ok_or_else(|| GitError::InvalidFormat("invalid fetch filter".into()))?;
343 validate_protocol_v2_token("fetch filter", filter)?;
344 out.filter = Some(filter.to_string());
345 }
346 value if value.starts_with("packfile-uris ") => {
347 if out.packfile_uris.is_some() {
348 return Err(GitError::InvalidFormat(
349 "fetch request has duplicate packfile-uris".into(),
350 ));
351 }
352 let protocols = value.strip_prefix("packfile-uris ").ok_or_else(|| {
353 GitError::InvalidFormat("invalid fetch packfile-uris".into())
354 })?;
355 validate_protocol_v2_token("fetch packfile-uris", protocols)?;
356 out.packfile_uris = Some(protocols.to_string());
357 }
358 other => {
359 return Err(GitError::InvalidFormat(format!(
360 "unsupported fetch argument {other}"
361 )));
362 }
363 }
364 }
365 Ok(out)
366 }
367
368 pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
369 let mut request = ProtocolV2CommandRequest::new("fetch")?;
370 for oid in &self.wants {
371 request.arguments.push(format!("want {oid}").into_bytes());
372 }
373 for name in &self.want_refs {
374 validate_protocol_v2_token("fetch want-ref", name)?;
375 request
376 .arguments
377 .push(format!("want-ref {name}").into_bytes());
378 }
379 for oid in &self.haves {
380 request.arguments.push(format!("have {oid}").into_bytes());
381 }
382 for oid in &self.shallow {
383 request
384 .arguments
385 .push(format!("shallow {oid}").into_bytes());
386 }
387 if let Some(deepen) = self.deepen {
388 if deepen == 0 {
389 return Err(GitError::InvalidFormat(
390 "fetch deepen must be positive".into(),
391 ));
392 }
393 request
394 .arguments
395 .push(format!("deepen {deepen}").into_bytes());
396 }
397 if let Some(deepen_since) = self.deepen_since {
398 request
399 .arguments
400 .push(format!("deepen-since {deepen_since}").into_bytes());
401 }
402 for name in &self.deepen_not {
403 validate_protocol_v2_token("fetch deepen-not", name)?;
404 request
405 .arguments
406 .push(format!("deepen-not {name}").into_bytes());
407 }
408 if self.deepen_relative {
409 request.arguments.push(b"deepen-relative".to_vec());
410 }
411 if let Some(filter) = &self.filter {
412 validate_protocol_v2_token("fetch filter", filter)?;
413 request
414 .arguments
415 .push(format!("filter {filter}").into_bytes());
416 }
417 if let Some(protocols) = &self.packfile_uris {
418 validate_protocol_v2_token("fetch packfile-uris", protocols)?;
419 request
420 .arguments
421 .push(format!("packfile-uris {protocols}").into_bytes());
422 }
423 if self.thin_pack {
424 request.arguments.push(b"thin-pack".to_vec());
425 }
426 if self.no_progress {
427 request.arguments.push(b"no-progress".to_vec());
428 }
429 if self.include_tag {
430 request.arguments.push(b"include-tag".to_vec());
431 }
432 if self.ofs_delta {
433 request.arguments.push(b"ofs-delta".to_vec());
434 }
435 if self.sideband_all {
436 request.arguments.push(b"sideband-all".to_vec());
437 }
438 if self.wait_for_done {
439 request.arguments.push(b"wait-for-done".to_vec());
440 }
441 if self.done {
442 request.arguments.push(b"done".to_vec());
443 }
444 Ok(request)
445 }
446}
447
448impl ProtocolV2ObjectInfoRequest {
449 pub fn from_command_request(
450 format: ObjectFormat,
451 request: &ProtocolV2CommandRequest,
452 ) -> Result<Self> {
453 if request.command != "object-info" {
454 return Err(GitError::InvalidFormat(format!(
455 "expected object-info command, got {}",
456 request.command
457 )));
458 }
459 let mut out = Self::default();
460 for argument in &request.arguments {
461 let text = parse_protocol_v2_line_text("object-info request argument", argument)?;
462 if text == "size" {
463 if out.size {
464 return Err(GitError::InvalidFormat(
465 "object-info request has duplicate size argument".into(),
466 ));
467 }
468 out.size = true;
469 } else if text.starts_with("oid ") {
470 out.oids
471 .push(parse_oid_argument(format, "object-info oid", text, "oid ")?);
472 } else {
473 return Err(GitError::InvalidFormat(format!(
474 "unsupported object-info request argument {text}"
475 )));
476 }
477 }
478 if !out.size {
479 return Err(GitError::InvalidFormat(
480 "object-info request is missing size argument".into(),
481 ));
482 }
483 if out.oids.is_empty() {
484 return Err(GitError::InvalidFormat(
485 "object-info request is missing object ids".into(),
486 ));
487 }
488 Ok(out)
489 }
490
491 pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
492 if !self.size {
493 return Err(GitError::InvalidFormat(
494 "object-info request is missing size argument".into(),
495 ));
496 }
497 if self.oids.is_empty() {
498 return Err(GitError::InvalidFormat(
499 "object-info request is missing object ids".into(),
500 ));
501 }
502 let mut request = ProtocolV2CommandRequest::new("object-info")?;
503 request.arguments.push(b"size".to_vec());
504 for oid in &self.oids {
505 request.arguments.push(format!("oid {oid}").into_bytes());
506 }
507 Ok(request)
508 }
509}
510
511pub fn parse_protocol_v2_advertisement(frames: &[PktLineFrame]) -> Result<TransportHandshake> {
512 let Some((first, rest)) = frames.split_first() else {
513 return Err(GitError::InvalidFormat(
514 "protocol v2 advertisement is empty".into(),
515 ));
516 };
517 match first {
518 PktLineFrame::Data(payload) if trim_trailing_lf(payload) == b"version 2" => {}
519 PktLineFrame::Data(_) => {
520 return Err(GitError::InvalidFormat(
521 "protocol v2 advertisement missing version line".into(),
522 ));
523 }
524 _ => {
525 return Err(GitError::InvalidFormat(
526 "protocol v2 advertisement must start with a data line".into(),
527 ));
528 }
529 }
530
531 let mut capabilities = Vec::new();
532 let mut saw_flush = false;
533 for (idx, frame) in rest.iter().enumerate() {
534 match frame {
535 PktLineFrame::Data(payload) => {
536 if saw_flush {
537 return Err(GitError::InvalidFormat(
538 "protocol v2 advertisement has data after flush".into(),
539 ));
540 }
541 capabilities.push(parse_protocol_v2_capability_line(payload)?);
542 }
543 PktLineFrame::Flush => {
544 saw_flush = true;
545 if idx + 1 != rest.len() {
546 return Err(GitError::InvalidFormat(
547 "protocol v2 advertisement has frames after flush".into(),
548 ));
549 }
550 }
551 PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
552 return Err(GitError::InvalidFormat(
553 "protocol v2 advertisement contains a non-flush control packet".into(),
554 ));
555 }
556 }
557 }
558 if !saw_flush {
559 return Err(GitError::InvalidFormat(
560 "protocol v2 advertisement missing flush".into(),
561 ));
562 }
563
564 Ok(TransportHandshake {
565 protocol: ProtocolVersion::V2,
566 capabilities,
567 })
568}
569
570pub fn encode_protocol_v2_advertisement(
571 handshake: &TransportHandshake,
572) -> Result<Vec<PktLineFrame>> {
573 if handshake.protocol != ProtocolVersion::V2 {
574 return Err(GitError::InvalidFormat(
575 "protocol v2 advertisement requires a v2 handshake".into(),
576 ));
577 }
578 let mut frames = vec![PktLineFrame::data(line_from_str("version 2"))?];
579 for capability in &handshake.capabilities {
580 frames.push(PktLineFrame::data(line(encode_protocol_v2_capability(
581 capability,
582 )?))?);
583 }
584 frames.push(PktLineFrame::Flush);
585 Ok(frames)
586}
587
588pub fn read_protocol_v2_advertisement(reader: &mut impl Read) -> Result<TransportHandshake> {
589 let frames = read_pkt_line_frames_until_flush(reader)?;
590 parse_protocol_v2_advertisement(&frames)
591}
592
593pub fn write_protocol_v2_advertisement(
594 writer: &mut impl Write,
595 handshake: &TransportHandshake,
596) -> Result<()> {
597 if handshake.protocol != ProtocolVersion::V2 {
598 return Err(GitError::InvalidFormat(
599 "protocol v2 advertisement requires a v2 handshake".into(),
600 ));
601 }
602 write_pkt_line_payload(writer, b"version 2\n")?;
603 for capability in &handshake.capabilities {
604 write_pkt_line_payload(writer, &line(encode_protocol_v2_capability(capability)?))?;
605 }
606 writer.write_all(b"0000")?;
607 Ok(())
608}
609
610pub fn trace_protocol_v2_advertisement_read(handshake: &TransportHandshake) -> Result<()> {
617 for frame in encode_protocol_v2_advertisement(handshake)? {
618 match frame {
619 PktLineFrame::Data(payload) => trace_packet_read_payload(&payload),
620 PktLineFrame::Flush => trace_packet_read_payload(b"0000"),
621 PktLineFrame::Delimiter => trace_packet_read_payload(b"0001"),
622 PktLineFrame::ResponseEnd => trace_packet_read_payload(b"0002"),
623 }
624 }
625 Ok(())
626}
627
628pub fn parse_protocol_v2_command_request(
629 frames: &[PktLineFrame],
630) -> Result<ProtocolV2CommandRequest> {
631 let Some((first, rest)) = frames.split_first() else {
632 return Err(GitError::InvalidFormat(
633 "protocol v2 command request is empty".into(),
634 ));
635 };
636 let command = match first {
637 PktLineFrame::Data(payload) => parse_protocol_v2_command_line(payload)?,
638 _ => {
639 return Err(GitError::InvalidFormat(
640 "protocol v2 command request must start with a command line".into(),
641 ));
642 }
643 };
644
645 let mut capabilities = Vec::new();
646 let mut arguments = Vec::new();
647 let mut in_arguments = false;
648 let mut saw_flush = false;
649 for (idx, frame) in rest.iter().enumerate() {
650 match frame {
651 PktLineFrame::Data(payload) if !in_arguments => {
652 if saw_flush {
653 return Err(GitError::InvalidFormat(
654 "protocol v2 command request has data after flush".into(),
655 ));
656 }
657 capabilities.push(parse_protocol_v2_capability_line(payload)?);
658 }
659 PktLineFrame::Data(payload) => {
660 if saw_flush {
661 return Err(GitError::InvalidFormat(
662 "protocol v2 command request has data after flush".into(),
663 ));
664 }
665 let argument = trim_trailing_lf(payload);
666 if argument.is_empty() {
667 return Err(GitError::InvalidFormat(
668 "protocol v2 command argument is empty".into(),
669 ));
670 }
671 if argument
672 .iter()
673 .any(|byte| matches!(*byte, b'\n' | b'\r' | 0))
674 {
675 return Err(GitError::InvalidFormat(
676 "protocol v2 command argument contains a delimiter byte".into(),
677 ));
678 }
679 arguments.push(argument.to_vec());
680 }
681 PktLineFrame::Delimiter => {
682 if in_arguments {
683 return Err(GitError::InvalidFormat(format!(
684 "expected flush after {} arguments",
685 command
686 )));
687 }
688 if saw_flush {
689 return Err(GitError::InvalidFormat(
690 "protocol v2 command request has delimiter after flush".into(),
691 ));
692 }
693 in_arguments = true;
694 }
695 PktLineFrame::Flush => {
696 saw_flush = true;
697 if idx + 1 != rest.len() {
698 return Err(GitError::InvalidFormat(
699 "protocol v2 command request has frames after flush".into(),
700 ));
701 }
702 }
703 PktLineFrame::ResponseEnd => {
704 return Err(GitError::InvalidFormat(
705 "protocol v2 command request contains response-end".into(),
706 ));
707 }
708 }
709 }
710 if !saw_flush {
711 return Err(GitError::InvalidFormat(
712 "protocol v2 command request missing flush".into(),
713 ));
714 }
715
716 Ok(ProtocolV2CommandRequest {
717 command,
718 capabilities,
719 arguments,
720 })
721}
722
723pub fn encode_protocol_v2_command_request(
724 request: &ProtocolV2CommandRequest,
725) -> Result<Vec<PktLineFrame>> {
726 validate_capability_name(&request.command)?;
727 let mut frames = Vec::new();
728 frames.push(PktLineFrame::data(line_from_str(&format!(
729 "command={}",
730 request.command
731 )))?);
732 for capability in &request.capabilities {
733 frames.push(PktLineFrame::data(line(encode_protocol_v2_capability(
734 capability,
735 )?))?);
736 }
737 if !request.arguments.is_empty() {
738 frames.push(PktLineFrame::Delimiter);
739 for argument in &request.arguments {
740 validate_protocol_v2_argument(argument)?;
741 let mut payload = argument.clone();
742 payload.push(b'\n');
743 frames.push(PktLineFrame::data(payload)?);
744 }
745 }
746 frames.push(PktLineFrame::Flush);
747 Ok(frames)
748}
749
750pub fn parse_protocol_v2_request(frames: &[PktLineFrame]) -> Result<ProtocolV2Request> {
751 if matches!(frames, [PktLineFrame::Flush]) {
752 return Ok(ProtocolV2Request::Done);
753 }
754 parse_protocol_v2_command_request(frames).map(ProtocolV2Request::Command)
755}
756
757pub fn encode_protocol_v2_request(request: &ProtocolV2Request) -> Result<Vec<PktLineFrame>> {
758 match request {
759 ProtocolV2Request::Command(command) => encode_protocol_v2_command_request(command),
760 ProtocolV2Request::Done => Ok(vec![PktLineFrame::Flush]),
761 }
762}
763
764pub fn read_protocol_v2_request(reader: &mut impl Read) -> Result<ProtocolV2Request> {
765 let frames = read_pkt_line_frames_until_flush(reader)?;
766 parse_protocol_v2_request(&frames)
767}
768
769pub fn write_protocol_v2_request(
770 writer: &mut impl Write,
771 request: &ProtocolV2Request,
772) -> Result<()> {
773 match request {
774 ProtocolV2Request::Command(command) => write_protocol_v2_command_request(writer, command),
775 ProtocolV2Request::Done => {
776 writer.write_all(b"0000")?;
777 Ok(())
778 }
779 }
780}
781
782pub fn read_protocol_v2_command_request(
783 reader: &mut impl Read,
784) -> Result<ProtocolV2CommandRequest> {
785 let mut frames = Vec::new();
786 loop {
787 let Some(frame) = read_pkt_line_frame(reader)? else {
788 if let Some(command) = frames.first().and_then(|frame| match frame {
789 PktLineFrame::Data(payload) => parse_protocol_v2_command_line(payload).ok(),
790 _ => None,
791 }) && frames
792 .iter()
793 .any(|frame| matches!(frame, PktLineFrame::Delimiter))
794 {
795 return Err(GitError::InvalidFormat(format!(
796 "expected flush after {} arguments",
797 command
798 )));
799 }
800 return Err(GitError::InvalidFormat(
801 "pkt-line stream ended before control packet".into(),
802 ));
803 };
804 let done = matches!(frame, PktLineFrame::Flush);
805 frames.push(frame);
806 if done {
807 break;
808 }
809 }
810 parse_protocol_v2_command_request(&frames)
811}
812
813pub fn write_protocol_v2_command_request(
814 writer: &mut impl Write,
815 request: &ProtocolV2CommandRequest,
816) -> Result<()> {
817 validate_capability_name(&request.command)?;
818 write_pkt_line_payload(
819 writer,
820 &line_from_str(&format!("command={}", request.command)),
821 )?;
822 for capability in &request.capabilities {
823 write_pkt_line_payload(writer, &line(encode_protocol_v2_capability(capability)?))?;
824 }
825 if !request.arguments.is_empty() {
826 write_pkt_line_frame(writer, &PktLineFrame::Delimiter)?;
827 for argument in &request.arguments {
828 validate_protocol_v2_argument(argument)?;
829 let mut payload = argument.clone();
830 payload.push(b'\n');
831 write_pkt_line_payload(writer, &payload)?;
832 }
833 }
834 writer.write_all(b"0000")?;
835 Ok(())
836}
837
838pub fn read_protocol_v2_ls_refs_request(reader: &mut impl Read) -> Result<ProtocolV2LsRefsRequest> {
839 let request = read_protocol_v2_command_request(reader)?;
840 ProtocolV2LsRefsRequest::from_command_request(&request)
841}
842
843pub fn write_protocol_v2_ls_refs_request(
844 writer: &mut impl Write,
845 request: &ProtocolV2LsRefsRequest,
846) -> Result<()> {
847 let command = request.to_command_request()?;
848 write_protocol_v2_command_request(writer, &command)
849}
850
851pub fn parse_protocol_v2_ls_refs_response(
852 format: ObjectFormat,
853 frames: &[PktLineFrame],
854) -> Result<Vec<ProtocolV2LsRefsRecord>> {
855 let mut records = Vec::new();
856 let mut saw_flush = false;
857 for (idx, frame) in frames.iter().enumerate() {
858 match frame {
859 PktLineFrame::Data(payload) => {
860 if saw_flush {
861 return Err(GitError::InvalidFormat(
862 "ls-refs response has data after flush".into(),
863 ));
864 }
865 records.push(parse_protocol_v2_ls_refs_line(format, payload)?);
866 }
867 PktLineFrame::Flush => {
868 saw_flush = true;
869 if !flush_terminates_protocol_v2_response(frames, idx) {
870 return Err(GitError::InvalidFormat(
871 "ls-refs response has frames after flush".into(),
872 ));
873 }
874 }
875 PktLineFrame::ResponseEnd if saw_flush && idx + 1 == frames.len() => {}
876 PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
877 return Err(GitError::InvalidFormat(
878 "ls-refs response contains a non-flush control packet".into(),
879 ));
880 }
881 }
882 }
883 if !saw_flush {
884 return Err(GitError::InvalidFormat(
885 "ls-refs response missing flush".into(),
886 ));
887 }
888 Ok(records)
889}
890
891pub fn encode_protocol_v2_ls_refs_response(
892 records: &[ProtocolV2LsRefsRecord],
893) -> Result<Vec<PktLineFrame>> {
894 let mut frames = Vec::new();
895 for record in records {
896 frames.push(PktLineFrame::data(line_from_str(
897 &format_protocol_v2_ls_refs_record(record)?,
898 ))?);
899 }
900 frames.push(PktLineFrame::Flush);
901 Ok(frames)
902}
903
904fn frames_start_with_protocol_v2_advertisement(frames: &[PktLineFrame]) -> bool {
905 matches!(
906 frames.first(),
907 Some(PktLineFrame::Data(payload)) if trim_trailing_lf(payload) == b"version 2"
908 )
909}
910
911fn skip_leading_protocol_v2_advertisement_if_present(
929 reader: &mut impl Read,
930 sideband_all: bool,
931) -> Result<Option<PktLineFrame>> {
932 let first = read_pkt_line_frame(reader)?.ok_or_else(|| {
933 GitError::InvalidFormat("protocol v2 response ended before first pkt-line".into())
934 })?;
935 let PktLineFrame::Data(payload) = &first else {
936 return Ok(Some(first));
937 };
938 if trim_trailing_lf(payload) != b"version 2" {
939 if sideband_all {
940 let packet = parse_sideband_packet(payload)?;
944 let demuxed = match packet.channel {
945 SideBandChannel::Data => PktLineFrame::Data(packet.data),
946 SideBandChannel::Progress => read_protocol_v2_fetch_metadata_frame(reader, true)?,
947 SideBandChannel::Fatal => {
948 let message = String::from_utf8_lossy(&packet.data).into_owned();
949 return Err(GitError::InvalidFormat(format!(
950 "sideband fatal: {message}"
951 )));
952 }
953 };
954 return Ok(Some(demuxed));
955 }
956 return Ok(Some(first));
957 }
958 loop {
959 match read_pkt_line_frame(reader)? {
960 Some(PktLineFrame::Flush) => return Ok(None),
961 Some(PktLineFrame::Data(_)) => {}
962 Some(_) => {
963 return Err(GitError::InvalidFormat(
964 "protocol v2 capability advertisement contains a non-flush control packet"
965 .into(),
966 ));
967 }
968 None => {
969 return Err(GitError::InvalidFormat(
970 "protocol v2 capability advertisement missing flush".into(),
971 ));
972 }
973 }
974 }
975}
976
977pub fn read_protocol_v2_stateless_rpc_payload_frames(
981 reader: &mut impl Read,
982) -> Result<Vec<PktLineFrame>> {
983 let mut frames = read_pkt_line_frames_until_flush(reader)?;
984 if frames_start_with_protocol_v2_advertisement(&frames) {
985 frames = read_pkt_line_frames_until_flush(reader)?;
986 }
987 Ok(frames)
988}
989
990pub fn read_protocol_v2_ls_refs_response(
991 format: ObjectFormat,
992 reader: &mut impl Read,
993) -> Result<Vec<ProtocolV2LsRefsRecord>> {
994 let frames = read_protocol_v2_stateless_rpc_payload_frames(reader)?;
995 parse_protocol_v2_ls_refs_response(format, &frames)
996}
997
998pub fn write_protocol_v2_ls_refs_response(
999 writer: &mut impl Write,
1000 records: &[ProtocolV2LsRefsRecord],
1001) -> Result<()> {
1002 for record in records {
1003 write_pkt_line_payload(
1004 writer,
1005 &line_from_str(&format_protocol_v2_ls_refs_record(record)?),
1006 )?;
1007 }
1008 writer.write_all(b"0000")?;
1009 Ok(())
1010}
1011
1012pub fn read_protocol_v2_ls_refs_response_until_response_end(
1013 format: ObjectFormat,
1014 reader: &mut impl Read,
1015) -> Result<Vec<ProtocolV2LsRefsRecord>> {
1016 let frames = read_pkt_line_frames_until_response_end(reader)?;
1017 parse_protocol_v2_ls_refs_response(format, &frames)
1018}
1019
1020pub fn write_protocol_v2_ls_refs_response_with_response_end(
1021 writer: &mut impl Write,
1022 records: &[ProtocolV2LsRefsRecord],
1023) -> Result<()> {
1024 write_protocol_v2_ls_refs_response(writer, records)?;
1025 writer.write_all(b"0002")?;
1026 Ok(())
1027}
1028
1029pub fn exchange_protocol_v2_ls_refs(
1030 format: ObjectFormat,
1031 reader: &mut impl Read,
1032 writer: &mut impl Write,
1033 request: &ProtocolV2LsRefsRequest,
1034) -> Result<Vec<ProtocolV2LsRefsRecord>> {
1035 write_protocol_v2_ls_refs_request(writer, request)?;
1036 writer.flush()?;
1037 read_protocol_v2_ls_refs_response(format, reader)
1038}
1039
1040pub fn protocol_v2_ls_refs_records_to_ref_advertisement_set(
1056 records: &[ProtocolV2LsRefsRecord],
1057) -> Result<RefAdvertisementSet> {
1058 let mut refs: Vec<RefAdvertisement> = Vec::new();
1059 let mut symrefs: Vec<Capability> = Vec::new();
1060 for record in records {
1061 match record {
1062 ProtocolV2LsRefsRecord::Ref(reference) => {
1063 validate_protocol_v2_token("ls-refs ref name", &reference.name)?;
1064 refs.push(RefAdvertisement {
1065 oid: reference.oid,
1066 name: reference.name.clone(),
1067 capabilities: Vec::new(),
1068 });
1069 if let Some(peeled) = &reference.peeled {
1070 refs.push(RefAdvertisement {
1071 oid: peeled.clone(),
1072 name: format!("{}^{{}}", reference.name),
1073 capabilities: Vec::new(),
1074 });
1075 }
1076 if let Some(target) = &reference.symref_target {
1077 symrefs.push(protocol_v2_symref_capability(&reference.name, target)?);
1078 }
1079 }
1080 ProtocolV2LsRefsRecord::Unborn {
1081 name,
1082 symref_target,
1083 ..
1084 } => {
1085 validate_protocol_v2_token("ls-refs ref name", name)?;
1086 if let Some(target) = symref_target {
1087 symrefs.push(protocol_v2_symref_capability(name, target)?);
1088 }
1089 }
1090 }
1091 }
1092 if !symrefs.is_empty() {
1093 if let Some(first) = refs.first_mut() {
1094 first.capabilities = symrefs;
1095 } else {
1096 return Err(GitError::InvalidFormat(
1097 "ls-refs response advertised symrefs without any concrete refs".into(),
1098 ));
1099 }
1100 }
1101 Ok(RefAdvertisementSet {
1102 protocol: ProtocolVersion::V2,
1103 refs,
1104 shallow: Vec::new(),
1105 })
1106}
1107
1108pub fn parse_protocol_v2_ls_refs_response_as_ref_advertisement_set(
1113 format: ObjectFormat,
1114 frames: &[PktLineFrame],
1115) -> Result<RefAdvertisementSet> {
1116 let records = parse_protocol_v2_ls_refs_response(format, frames)?;
1117 protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
1118}
1119
1120pub fn read_protocol_v2_ls_refs_response_as_ref_advertisement_set(
1123 format: ObjectFormat,
1124 reader: &mut impl Read,
1125) -> Result<RefAdvertisementSet> {
1126 let records = read_protocol_v2_ls_refs_response(format, reader)?;
1127 protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
1128}
1129
1130fn protocol_v2_symref_capability(name: &str, target: &str) -> Result<Capability> {
1131 validate_protocol_v2_token("ls-refs symref-target", target)?;
1132 Ok(Capability {
1133 name: "symref".into(),
1134 value: Some(format!("{name}:{target}")),
1135 })
1136}
1137
1138pub fn read_protocol_v2_fetch_request(
1139 format: ObjectFormat,
1140 reader: &mut impl Read,
1141) -> Result<ProtocolV2FetchRequest> {
1142 let request = read_protocol_v2_command_request(reader)?;
1143 ProtocolV2FetchRequest::from_command_request(format, &request)
1144}
1145
1146pub fn write_protocol_v2_fetch_request(
1147 writer: &mut impl Write,
1148 request: &ProtocolV2FetchRequest,
1149) -> Result<()> {
1150 let command = request.to_command_request()?;
1151 write_protocol_v2_command_request(writer, &command)
1152}
1153
1154pub fn read_protocol_v2_object_info_request(
1155 format: ObjectFormat,
1156 reader: &mut impl Read,
1157) -> Result<ProtocolV2ObjectInfoRequest> {
1158 let request = read_protocol_v2_command_request(reader)?;
1159 ProtocolV2ObjectInfoRequest::from_command_request(format, &request)
1160}
1161
1162pub fn write_protocol_v2_object_info_request(
1163 writer: &mut impl Write,
1164 request: &ProtocolV2ObjectInfoRequest,
1165) -> Result<()> {
1166 let command = request.to_command_request()?;
1167 write_protocol_v2_command_request(writer, &command)
1168}
1169
1170pub fn parse_protocol_v2_fetch_response(
1171 format: ObjectFormat,
1172 frames: &[PktLineFrame],
1173) -> Result<Vec<ProtocolV2FetchResponseSection>> {
1174 let mut sections = Vec::new();
1175 let mut current: Option<(String, Vec<Vec<u8>>)> = None;
1176 let mut saw_flush = false;
1177 for (idx, frame) in frames.iter().enumerate() {
1178 match frame {
1179 PktLineFrame::Data(payload) => {
1180 if saw_flush {
1181 return Err(GitError::InvalidFormat(
1182 "fetch response has data after flush".into(),
1183 ));
1184 }
1185 if let Some((_name, lines)) = &mut current {
1186 lines.push(payload.clone());
1187 } else {
1188 let name = parse_fetch_section_header(payload)?;
1189 current = Some((name, Vec::new()));
1190 }
1191 }
1192 PktLineFrame::Delimiter => {
1193 if saw_flush {
1194 return Err(GitError::InvalidFormat(
1195 "fetch response has delimiter after flush".into(),
1196 ));
1197 }
1198 let Some((name, lines)) = current.take() else {
1199 return Err(GitError::InvalidFormat(
1200 "fetch response has delimiter before section".into(),
1201 ));
1202 };
1203 sections.push(parse_fetch_section(format, name, lines)?);
1204 }
1205 PktLineFrame::Flush => {
1206 saw_flush = true;
1207 if !flush_terminates_protocol_v2_response(frames, idx) {
1208 return Err(GitError::InvalidFormat(
1209 "fetch response has frames after flush".into(),
1210 ));
1211 }
1212 if let Some((name, lines)) = current.take() {
1213 sections.push(parse_fetch_section(format, name, lines)?);
1214 }
1215 }
1216 PktLineFrame::ResponseEnd if saw_flush && idx + 1 == frames.len() => {}
1217 PktLineFrame::ResponseEnd => {
1218 return Err(GitError::InvalidFormat(
1219 "fetch response contains response-end".into(),
1220 ));
1221 }
1222 }
1223 }
1224 if !saw_flush {
1225 return Err(GitError::InvalidFormat(
1226 "fetch response missing flush".into(),
1227 ));
1228 }
1229 Ok(sections)
1230}
1231
1232pub fn encode_protocol_v2_fetch_response(
1233 sections: &[ProtocolV2FetchResponseSection],
1234) -> Result<Vec<PktLineFrame>> {
1235 let mut frames = Vec::new();
1236 for (idx, section) in sections.iter().enumerate() {
1237 if idx != 0 {
1238 frames.push(PktLineFrame::Delimiter);
1239 }
1240 frames.push(PktLineFrame::data(line_from_str(
1241 protocol_v2_fetch_section_name(section),
1242 ))?);
1243 for line in format_protocol_v2_fetch_section_lines(section)? {
1244 frames.push(PktLineFrame::data(line)?);
1245 }
1246 }
1247 frames.push(PktLineFrame::Flush);
1248 Ok(frames)
1249}
1250
1251pub fn parse_protocol_v2_fetch_sideband_all_response(
1252 format: ObjectFormat,
1253 frames: &[PktLineFrame],
1254) -> Result<ProtocolV2FetchSidebandAllResponse> {
1255 let mut demuxed = Vec::new();
1256 let mut progress = Vec::new();
1257 let mut in_packfile = false;
1258 for frame in frames {
1259 match frame {
1260 PktLineFrame::Data(payload) if in_packfile => {
1261 demuxed.push(PktLineFrame::Data(payload.clone()));
1262 }
1263 PktLineFrame::Data(payload) => {
1264 let packet = parse_sideband_packet(payload)?;
1265 match packet.channel {
1266 SideBandChannel::Data => {
1267 if trim_trailing_lf(&packet.data) == b"packfile" {
1268 in_packfile = true;
1269 }
1270 demuxed.push(PktLineFrame::Data(packet.data));
1271 }
1272 SideBandChannel::Progress => progress.push(packet.data),
1273 SideBandChannel::Fatal => {
1274 let message = String::from_utf8_lossy(&packet.data).into_owned();
1275 return Err(GitError::InvalidFormat(format!(
1276 "sideband fatal: {message}"
1277 )));
1278 }
1279 }
1280 }
1281 PktLineFrame::Delimiter => {
1282 in_packfile = false;
1283 demuxed.push(PktLineFrame::Delimiter);
1284 }
1285 PktLineFrame::Flush => {
1286 in_packfile = false;
1287 demuxed.push(PktLineFrame::Flush);
1288 }
1289 PktLineFrame::ResponseEnd => {
1290 in_packfile = false;
1291 demuxed.push(PktLineFrame::ResponseEnd);
1292 }
1293 }
1294 }
1295 Ok(ProtocolV2FetchSidebandAllResponse {
1296 sections: parse_protocol_v2_fetch_response(format, &demuxed)?,
1297 progress,
1298 })
1299}
1300
1301pub fn encode_protocol_v2_fetch_sideband_all_response(
1302 sections: &[ProtocolV2FetchResponseSection],
1303) -> Result<Vec<PktLineFrame>> {
1304 let frames = encode_protocol_v2_fetch_response(sections)?;
1305 let mut encoded = Vec::new();
1306 let mut in_packfile = false;
1307 for frame in frames {
1308 match frame {
1309 PktLineFrame::Data(payload) if in_packfile => {
1310 encoded.push(PktLineFrame::Data(payload));
1311 }
1312 PktLineFrame::Data(payload) => {
1313 if trim_trailing_lf(&payload) == b"packfile" {
1314 in_packfile = true;
1315 }
1316 encoded.push(PktLineFrame::data(encode_sideband_packet(
1317 &SideBandPacket {
1318 channel: SideBandChannel::Data,
1319 data: payload,
1320 },
1321 )?)?);
1322 }
1323 PktLineFrame::Delimiter => {
1324 in_packfile = false;
1325 encoded.push(PktLineFrame::Delimiter);
1326 }
1327 PktLineFrame::Flush => {
1328 in_packfile = false;
1329 encoded.push(PktLineFrame::Flush);
1330 }
1331 PktLineFrame::ResponseEnd => {
1332 in_packfile = false;
1333 encoded.push(PktLineFrame::ResponseEnd);
1334 }
1335 }
1336 }
1337 Ok(encoded)
1338}
1339
1340pub fn read_protocol_v2_fetch_response(
1341 format: ObjectFormat,
1342 reader: &mut impl Read,
1343) -> Result<Vec<ProtocolV2FetchResponseSection>> {
1344 let frames = read_protocol_v2_stateless_rpc_payload_frames(reader)?;
1345 parse_protocol_v2_fetch_response(format, &frames)
1346}
1347
1348pub fn read_protocol_v2_fetch_response_header(
1349 format: ObjectFormat,
1350 reader: &mut impl Read,
1351 sideband_all: bool,
1352) -> Result<ProtocolV2FetchResponseHeader> {
1353 let mut pending = skip_leading_protocol_v2_advertisement_if_present(reader, sideband_all)?;
1354 let mut sections = Vec::new();
1355 let mut current: Option<(String, Vec<Vec<u8>>)> = None;
1356 loop {
1357 let frame = if let Some(frame) = pending.take() {
1358 frame
1359 } else {
1360 read_protocol_v2_fetch_metadata_frame(reader, sideband_all)?
1361 };
1362 match frame {
1363 PktLineFrame::Data(payload) => {
1364 if let Some((_name, lines)) = &mut current {
1365 lines.push(payload);
1366 } else {
1367 let name = parse_fetch_section_header(&payload)?;
1368 if name == "packfile" {
1369 return Ok(ProtocolV2FetchResponseHeader {
1370 sections,
1371 has_packfile: true,
1372 });
1373 }
1374 current = Some((name, Vec::new()));
1375 }
1376 }
1377 PktLineFrame::Delimiter => {
1378 let Some((name, lines)) = current.take() else {
1379 return Err(GitError::InvalidFormat(
1380 "fetch response has delimiter before section".into(),
1381 ));
1382 };
1383 sections.push(parse_fetch_section(format, name, lines)?);
1384 }
1385 PktLineFrame::Flush => {
1386 if let Some((name, lines)) = current.take() {
1387 sections.push(parse_fetch_section(format, name, lines)?);
1388 }
1389 return Ok(ProtocolV2FetchResponseHeader {
1390 sections,
1391 has_packfile: false,
1392 });
1393 }
1394 PktLineFrame::ResponseEnd => {
1395 return Err(GitError::InvalidFormat(
1396 "fetch response contains response-end".into(),
1397 ));
1398 }
1399 }
1400 }
1401}
1402
1403pub fn read_protocol_v2_fetch_negotiation_response(
1412 format: ObjectFormat,
1413 reader: &mut impl Read,
1414 sideband_all: bool,
1415 wait_for_done: bool,
1416) -> Result<ProtocolV2FetchNegotiationResponse> {
1417 let mut pending = skip_leading_protocol_v2_advertisement_if_present(reader, sideband_all)?;
1418 let first = if let Some(frame) = pending.take() {
1419 frame
1420 } else {
1421 read_protocol_v2_fetch_metadata_frame(reader, sideband_all)?
1422 };
1423 let PktLineFrame::Data(header) = first else {
1424 return Err(GitError::InvalidFormat(
1425 "fetch negotiation response is missing acknowledgments".into(),
1426 ));
1427 };
1428 if parse_fetch_section_header(&header)? != "acknowledgments" {
1429 return Err(GitError::InvalidFormat(
1430 "fetch negotiation response must begin with acknowledgments".into(),
1431 ));
1432 }
1433
1434 let mut acknowledgments = Vec::new();
1435 loop {
1436 match read_protocol_v2_fetch_metadata_frame(reader, sideband_all)? {
1437 PktLineFrame::Data(line) => {
1438 acknowledgments.push(parse_fetch_acknowledgment(format, &line)?);
1439 }
1440 PktLineFrame::Delimiter => {
1441 let ready = acknowledgments
1442 .iter()
1443 .any(|ack| matches!(ack, ProtocolV2FetchAcknowledgment::Ready));
1444 if !ready {
1445 return Err(GitError::InvalidFormat(
1446 "expected no other sections to be sent after no 'ready'".into(),
1447 ));
1448 }
1449 return Ok(ProtocolV2FetchNegotiationResponse {
1450 acknowledgments,
1451 has_following_sections: true,
1452 });
1453 }
1454 PktLineFrame::Flush => {
1455 let ready = acknowledgments
1456 .iter()
1457 .any(|ack| matches!(ack, ProtocolV2FetchAcknowledgment::Ready));
1458 if ready && !wait_for_done {
1459 return Err(GitError::InvalidFormat(
1460 "expected packfile to be sent after 'ready'".into(),
1461 ));
1462 }
1463 return Ok(ProtocolV2FetchNegotiationResponse {
1464 acknowledgments,
1465 has_following_sections: false,
1466 });
1467 }
1468 PktLineFrame::ResponseEnd => {
1469 return Err(GitError::InvalidFormat(
1470 "fetch negotiation response contains response-end".into(),
1471 ));
1472 }
1473 }
1474 }
1475}
1476
1477fn read_protocol_v2_fetch_metadata_frame(
1478 reader: &mut impl Read,
1479 sideband_all: bool,
1480) -> Result<PktLineFrame> {
1481 loop {
1482 let frame = read_pkt_line_frame(reader)?
1483 .ok_or_else(|| GitError::InvalidFormat("fetch response ended before flush".into()))?;
1484 if sideband_all && let PktLineFrame::Data(payload) = frame {
1485 let packet = parse_sideband_packet(&payload)?;
1486 match packet.channel {
1487 SideBandChannel::Data => return Ok(PktLineFrame::Data(packet.data)),
1488 SideBandChannel::Progress => continue,
1489 SideBandChannel::Fatal => {
1490 let message = String::from_utf8_lossy(&packet.data).into_owned();
1491 return Err(GitError::InvalidFormat(format!(
1492 "sideband fatal: {message}"
1493 )));
1494 }
1495 }
1496 }
1497 return Ok(frame);
1498 }
1499}
1500
1501pub fn write_protocol_v2_fetch_response(
1502 writer: &mut impl Write,
1503 sections: &[ProtocolV2FetchResponseSection],
1504) -> Result<()> {
1505 write_protocol_v2_fetch_response_inner(writer, sections, false, false)
1506}
1507
1508pub fn read_protocol_v2_fetch_sideband_all_response(
1509 format: ObjectFormat,
1510 reader: &mut impl Read,
1511) -> Result<ProtocolV2FetchSidebandAllResponse> {
1512 let frames = read_protocol_v2_stateless_rpc_payload_frames(reader)?;
1513 parse_protocol_v2_fetch_sideband_all_response(format, &frames)
1514}
1515
1516pub fn write_protocol_v2_fetch_sideband_all_response(
1517 writer: &mut impl Write,
1518 sections: &[ProtocolV2FetchResponseSection],
1519) -> Result<()> {
1520 write_protocol_v2_fetch_response_inner(writer, sections, true, false)
1521}
1522
1523pub fn read_protocol_v2_fetch_response_until_response_end(
1524 format: ObjectFormat,
1525 reader: &mut impl Read,
1526) -> Result<Vec<ProtocolV2FetchResponseSection>> {
1527 let frames = read_pkt_line_frames_until_response_end(reader)?;
1528 parse_protocol_v2_fetch_response(format, &frames)
1529}
1530
1531pub fn write_protocol_v2_fetch_response_with_response_end(
1532 writer: &mut impl Write,
1533 sections: &[ProtocolV2FetchResponseSection],
1534) -> Result<()> {
1535 write_protocol_v2_fetch_response_inner(writer, sections, false, true)
1536}
1537
1538pub fn read_protocol_v2_fetch_sideband_all_response_until_response_end(
1539 format: ObjectFormat,
1540 reader: &mut impl Read,
1541) -> Result<ProtocolV2FetchSidebandAllResponse> {
1542 let frames = read_pkt_line_frames_until_response_end(reader)?;
1543 parse_protocol_v2_fetch_sideband_all_response(format, &frames)
1544}
1545
1546pub fn write_protocol_v2_fetch_sideband_all_response_with_response_end(
1547 writer: &mut impl Write,
1548 sections: &[ProtocolV2FetchResponseSection],
1549) -> Result<()> {
1550 write_protocol_v2_fetch_response_inner(writer, sections, true, true)
1551}
1552
1553fn write_protocol_v2_fetch_response_inner(
1554 writer: &mut impl Write,
1555 sections: &[ProtocolV2FetchResponseSection],
1556 sideband_all: bool,
1557 response_end: bool,
1558) -> Result<()> {
1559 let mut in_packfile = false;
1560 for (idx, section) in sections.iter().enumerate() {
1561 if idx != 0 {
1562 in_packfile = false;
1563 write_pkt_line_frame(writer, &PktLineFrame::Delimiter)?;
1564 }
1565 write_protocol_v2_fetch_payload(
1566 writer,
1567 &line_from_str(protocol_v2_fetch_section_name(section)),
1568 sideband_all,
1569 &mut in_packfile,
1570 )?;
1571 for payload in format_protocol_v2_fetch_section_lines(section)? {
1572 write_protocol_v2_fetch_payload(writer, &payload, sideband_all, &mut in_packfile)?;
1573 }
1574 }
1575 writer.write_all(b"0000")?;
1576 if response_end {
1577 writer.write_all(b"0002")?;
1578 }
1579 Ok(())
1580}
1581
1582fn write_protocol_v2_fetch_payload(
1583 writer: &mut impl Write,
1584 payload: &[u8],
1585 sideband_all: bool,
1586 in_packfile: &mut bool,
1587) -> Result<()> {
1588 if sideband_all && !*in_packfile {
1589 if trim_trailing_lf(payload) == b"packfile" {
1590 *in_packfile = true;
1591 }
1592 write_sideband_payload(writer, SideBandChannel::Data, payload)
1593 } else {
1594 write_pkt_line_payload(writer, payload)
1595 }
1596}
1597
1598pub fn exchange_protocol_v2_fetch(
1599 format: ObjectFormat,
1600 reader: &mut impl Read,
1601 writer: &mut impl Write,
1602 request: &ProtocolV2FetchRequest,
1603) -> Result<Vec<ProtocolV2FetchResponseSection>> {
1604 write_protocol_v2_fetch_request(writer, request)?;
1605 writer.flush()?;
1606 read_protocol_v2_fetch_response(format, reader)
1607}
1608
1609pub fn parse_protocol_v2_object_info_response(
1610 format: ObjectFormat,
1611 frames: &[PktLineFrame],
1612) -> Result<ProtocolV2ObjectInfoResponse> {
1613 let Some((first, rest)) = frames.split_first() else {
1614 return Err(GitError::InvalidFormat(
1615 "object-info response is empty".into(),
1616 ));
1617 };
1618 let PktLineFrame::Data(attrs) = first else {
1619 return Err(GitError::InvalidFormat(
1620 "object-info response must start with attributes".into(),
1621 ));
1622 };
1623 let attrs = parse_protocol_v2_line_text("object-info response attributes", attrs)?;
1624 let mut response = ProtocolV2ObjectInfoResponse::default();
1625 for attr in attrs.split(' ') {
1626 validate_protocol_v2_token("object-info response attribute", attr)?;
1627 match attr {
1628 "size" => {
1629 if response.size {
1630 return Err(GitError::InvalidFormat(
1631 "object-info response has duplicate size attribute".into(),
1632 ));
1633 }
1634 response.size = true;
1635 }
1636 other => {
1637 return Err(GitError::InvalidFormat(format!(
1638 "unsupported object-info response attribute {other}"
1639 )));
1640 }
1641 }
1642 }
1643 if !response.size {
1644 return Err(GitError::InvalidFormat(
1645 "object-info response is missing size attribute".into(),
1646 ));
1647 }
1648
1649 let mut saw_flush = false;
1650 for (idx, frame) in rest.iter().enumerate() {
1651 match frame {
1652 PktLineFrame::Data(payload) if !saw_flush => {
1653 response
1654 .records
1655 .push(parse_protocol_v2_object_info_record(format, payload)?);
1656 }
1657 PktLineFrame::Data(_) => {
1658 return Err(GitError::InvalidFormat(
1659 "object-info response has data after flush".into(),
1660 ));
1661 }
1662 PktLineFrame::Flush => {
1663 saw_flush = true;
1664 if idx + 1 != rest.len() {
1665 return Err(GitError::InvalidFormat(
1666 "object-info response has frames after flush".into(),
1667 ));
1668 }
1669 }
1670 PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
1671 return Err(GitError::InvalidFormat(
1672 "object-info response contains a non-flush control packet".into(),
1673 ));
1674 }
1675 }
1676 }
1677 if !saw_flush {
1678 return Err(GitError::InvalidFormat(
1679 "object-info response missing flush".into(),
1680 ));
1681 }
1682 Ok(response)
1683}
1684
1685pub fn encode_protocol_v2_object_info_response(
1686 response: &ProtocolV2ObjectInfoResponse,
1687) -> Result<Vec<PktLineFrame>> {
1688 if !response.size {
1689 return Err(GitError::InvalidFormat(
1690 "object-info response is missing size attribute".into(),
1691 ));
1692 }
1693 let mut frames = Vec::new();
1694 frames.push(PktLineFrame::data(line_from_str("size"))?);
1695 for record in &response.records {
1696 frames.push(PktLineFrame::data(line_from_str(&format!(
1697 "{} {}",
1698 record.oid, record.size
1699 )))?);
1700 }
1701 frames.push(PktLineFrame::Flush);
1702 Ok(frames)
1703}
1704
1705pub fn read_protocol_v2_object_info_response(
1706 format: ObjectFormat,
1707 reader: &mut impl Read,
1708) -> Result<ProtocolV2ObjectInfoResponse> {
1709 let frames = read_pkt_line_frames_until_flush(reader)?;
1710 parse_protocol_v2_object_info_response(format, &frames)
1711}
1712
1713pub fn write_protocol_v2_object_info_response(
1714 writer: &mut impl Write,
1715 response: &ProtocolV2ObjectInfoResponse,
1716) -> Result<()> {
1717 if !response.size {
1718 return Err(GitError::InvalidFormat(
1719 "object-info response is missing size attribute".into(),
1720 ));
1721 }
1722 write_pkt_line_payload(writer, b"size\n")?;
1723 for record in &response.records {
1724 write_pkt_line_payload(
1725 writer,
1726 &line_from_str(&format!("{} {}", record.oid, record.size)),
1727 )?;
1728 }
1729 writer.write_all(b"0000")?;
1730 Ok(())
1731}
1732
1733pub fn exchange_protocol_v2_object_info(
1734 format: ObjectFormat,
1735 reader: &mut impl Read,
1736 writer: &mut impl Write,
1737 request: &ProtocolV2ObjectInfoRequest,
1738) -> Result<ProtocolV2ObjectInfoResponse> {
1739 write_protocol_v2_object_info_request(writer, request)?;
1740 writer.flush()?;
1741 read_protocol_v2_object_info_response(format, reader)
1742}
1743
1744pub fn demux_protocol_v2_fetch_packfile(
1745 sections: &[ProtocolV2FetchResponseSection],
1746) -> Result<Option<SideBandDemux>> {
1747 let mut packfile = None;
1748 for section in sections {
1749 if let ProtocolV2FetchResponseSection::Packfile(lines) = section {
1750 if packfile.is_some() {
1751 return Err(GitError::InvalidFormat(
1752 "fetch response has duplicate packfile sections".into(),
1753 ));
1754 }
1755 packfile = Some(parse_and_demux_sideband_packets(lines)?);
1756 }
1757 }
1758 Ok(packfile)
1759}
1760
1761pub fn protocol_v2_object_format(capabilities: &[Capability]) -> Result<ObjectFormat> {
1762 let mut format = None;
1763 for capability in capabilities {
1764 if capability.name != "object-format" {
1765 continue;
1766 }
1767 if format.is_some() {
1768 return Err(GitError::InvalidFormat(
1769 "protocol v2 has duplicate object-format capabilities".into(),
1770 ));
1771 }
1772 let Some(value) = &capability.value else {
1773 return Err(GitError::InvalidFormat(
1774 "protocol v2 object-format capability is missing a value".into(),
1775 ));
1776 };
1777 format = Some(value.parse::<ObjectFormat>()?);
1778 }
1779 Ok(format.unwrap_or(ObjectFormat::Sha1))
1780}
1781
1782pub fn validate_protocol_v2_command_request_capabilities(
1783 handshake: &TransportHandshake,
1784 request: &ProtocolV2CommandRequest,
1785) -> Result<()> {
1786 if handshake.protocol != ProtocolVersion::V2 {
1787 return Err(GitError::InvalidFormat(
1788 "protocol v2 command validation requires a v2 handshake".into(),
1789 ));
1790 }
1791 let advertised =
1792 protocol_v2_capability(&handshake.capabilities, &request.command).ok_or_else(|| {
1793 GitError::InvalidFormat(format!("unadvertised command {}", request.command))
1794 })?;
1795 if advertised.name.is_empty() {
1796 return Err(GitError::InvalidFormat(
1797 "advertised command capability is empty".into(),
1798 ));
1799 }
1800 parse_protocol_v2_command_options(&request.capabilities)?;
1801
1802 for capability in &request.capabilities {
1803 let advertised = protocol_v2_capability(&handshake.capabilities, &capability.name)
1804 .ok_or_else(|| {
1805 GitError::InvalidFormat(format!(
1806 "unadvertised protocol v2 capability {}",
1807 capability.name
1808 ))
1809 })?;
1810 if capability.name == "object-format" {
1811 validate_protocol_v2_object_format_request(advertised, capability)?;
1812 }
1813 }
1814 Ok(())
1815}
1816
1817pub fn parse_protocol_v2_command_options(
1818 capabilities: &[Capability],
1819) -> Result<ProtocolV2CommandOptions> {
1820 let mut out = ProtocolV2CommandOptions::default();
1821 for capability in capabilities {
1822 match capability.name.as_str() {
1823 "agent" => {
1824 if out.agent.is_some() {
1825 return Err(GitError::InvalidFormat(
1826 "protocol v2 command has duplicate agent capabilities".into(),
1827 ));
1828 }
1829 let Some(value) = &capability.value else {
1830 return Err(GitError::InvalidFormat(
1831 "protocol v2 agent capability is missing a value".into(),
1832 ));
1833 };
1834 validate_protocol_v2_capability_value(value)?;
1835 out.agent = Some(value.clone());
1836 }
1837 "object-format" => {
1838 if out.object_format.is_some() {
1839 return Err(GitError::InvalidFormat(
1840 "protocol v2 command has duplicate object-format capabilities".into(),
1841 ));
1842 }
1843 let Some(value) = &capability.value else {
1844 return Err(GitError::InvalidFormat(
1845 "protocol v2 object-format capability is missing a value".into(),
1846 ));
1847 };
1848 out.object_format = Some(value.parse::<ObjectFormat>()?);
1849 }
1850 "server-option" => {
1851 let Some(value) = &capability.value else {
1852 return Err(GitError::InvalidFormat(
1853 "protocol v2 server-option capability is missing a value".into(),
1854 ));
1855 };
1856 validate_protocol_v2_capability_value(value)?;
1857 out.server_options.push(value.clone());
1858 }
1859 _ => out.extra.push(capability.clone()),
1860 }
1861 }
1862 Ok(out)
1863}
1864
1865pub fn encode_protocol_v2_command_options(
1866 options: &ProtocolV2CommandOptions,
1867) -> Result<Vec<Capability>> {
1868 let mut capabilities = Vec::new();
1869 if let Some(agent) = &options.agent {
1870 validate_protocol_v2_capability_value(agent)?;
1871 capabilities.push(Capability {
1872 name: "agent".into(),
1873 value: Some(agent.clone()),
1874 });
1875 }
1876 if let Some(format) = options.object_format {
1877 capabilities.push(Capability {
1878 name: "object-format".into(),
1879 value: Some(format.name().into()),
1880 });
1881 }
1882 for option in &options.server_options {
1883 validate_protocol_v2_capability_value(option)?;
1884 capabilities.push(Capability {
1885 name: "server-option".into(),
1886 value: Some(option.clone()),
1887 });
1888 }
1889 for capability in &options.extra {
1890 if matches!(
1891 capability.name.as_str(),
1892 "agent" | "object-format" | "server-option"
1893 ) {
1894 return Err(GitError::InvalidFormat(format!(
1895 "protocol v2 extra capability duplicates known capability {}",
1896 capability.name
1897 )));
1898 }
1899 encode_protocol_v2_capability(capability)?;
1900 capabilities.push(capability.clone());
1901 }
1902 Ok(capabilities)
1903}
1904
1905pub fn parse_protocol_v2_ls_refs_features(
1906 capabilities: &[Capability],
1907) -> Result<Option<ProtocolV2LsRefsFeatures>> {
1908 let mut ls_refs = None;
1909 for capability in capabilities {
1910 if capability.name != "ls-refs" {
1911 continue;
1912 }
1913 if ls_refs.is_some() {
1914 return Err(GitError::InvalidFormat(
1915 "protocol v2 has duplicate ls-refs capabilities".into(),
1916 ));
1917 }
1918 ls_refs = Some(parse_protocol_v2_ls_refs_feature_value(
1919 capability.value.as_deref(),
1920 )?);
1921 }
1922 Ok(ls_refs)
1923}
1924
1925pub fn encode_protocol_v2_ls_refs_capability(
1926 features: &ProtocolV2LsRefsFeatures,
1927) -> Result<Capability> {
1928 let mut values = Vec::new();
1929 if features.unborn {
1930 values.push("unborn".to_string());
1931 }
1932 for feature in &features.unknown {
1933 validate_protocol_v2_token("ls-refs feature", feature)?;
1934 if feature == "unborn" {
1935 return Err(GitError::InvalidFormat(
1936 "ls-refs unknown features must not duplicate known feature unborn".into(),
1937 ));
1938 }
1939 values.push(feature.clone());
1940 }
1941 Ok(Capability {
1942 name: "ls-refs".into(),
1943 value: (!values.is_empty()).then(|| values.join(" ")),
1944 })
1945}
1946
1947pub fn validate_protocol_v2_ls_refs_request_features(
1948 features: &ProtocolV2LsRefsFeatures,
1949 request: &ProtocolV2LsRefsRequest,
1950) -> Result<()> {
1951 if request.unborn && !features.unborn {
1952 return Err(GitError::InvalidFormat(
1953 "ls-refs request uses unborn without advertised unborn feature".into(),
1954 ));
1955 }
1956 Ok(())
1957}
1958
1959pub fn validate_protocol_v2_ls_refs_command_request(
1960 handshake: &TransportHandshake,
1961 request: &ProtocolV2CommandRequest,
1962) -> Result<ProtocolV2LsRefsRequest> {
1963 validate_protocol_v2_command_request_capabilities(handshake, request)?;
1964 let ls_refs = ProtocolV2LsRefsRequest::from_command_request(request)?;
1965 let features = parse_protocol_v2_ls_refs_features(&handshake.capabilities)?
1966 .ok_or_else(|| GitError::InvalidFormat("ls-refs command was not advertised".into()))?;
1967 validate_protocol_v2_ls_refs_request_features(&features, &ls_refs)?;
1968 Ok(ls_refs)
1969}
1970
1971pub fn parse_protocol_v2_fetch_features(
1972 capabilities: &[Capability],
1973) -> Result<Option<ProtocolV2FetchFeatures>> {
1974 let mut fetch = None;
1975 for capability in capabilities {
1976 if capability.name != "fetch" {
1977 continue;
1978 }
1979 if fetch.is_some() {
1980 return Err(GitError::InvalidFormat(
1981 "protocol v2 has duplicate fetch capabilities".into(),
1982 ));
1983 }
1984 fetch = Some(parse_protocol_v2_fetch_feature_value(
1985 capability.value.as_deref(),
1986 )?);
1987 }
1988 Ok(fetch)
1989}
1990
1991pub fn encode_protocol_v2_fetch_capability(
1992 features: &ProtocolV2FetchFeatures,
1993) -> Result<Capability> {
1994 let mut values = Vec::new();
1995 if features.shallow {
1996 values.push("shallow".to_string());
1997 }
1998 if features.wait_for_done {
1999 values.push("wait-for-done".to_string());
2000 }
2001 if features.filter {
2002 values.push("filter".to_string());
2003 }
2004 if features.ref_in_want {
2005 values.push("ref-in-want".to_string());
2006 }
2007 if features.sideband_all {
2008 values.push("sideband-all".to_string());
2009 }
2010 if features.packfile_uris {
2011 values.push("packfile-uris".to_string());
2012 }
2013 for feature in &features.unknown {
2014 validate_protocol_v2_token("fetch feature", feature)?;
2015 if matches!(
2016 feature.as_str(),
2017 "shallow"
2018 | "wait-for-done"
2019 | "filter"
2020 | "ref-in-want"
2021 | "sideband-all"
2022 | "packfile-uris"
2023 ) {
2024 return Err(GitError::InvalidFormat(format!(
2025 "fetch unknown features must not duplicate known feature {feature}"
2026 )));
2027 }
2028 values.push(feature.clone());
2029 }
2030 Ok(Capability {
2031 name: "fetch".into(),
2032 value: (!values.is_empty()).then(|| values.join(" ")),
2033 })
2034}
2035
2036pub fn validate_protocol_v2_fetch_request_features(
2037 features: &ProtocolV2FetchFeatures,
2038 request: &ProtocolV2FetchRequest,
2039) -> Result<()> {
2040 if !features.shallow
2041 && (!request.shallow.is_empty()
2042 || request.deepen.is_some()
2043 || request.deepen_since.is_some()
2044 || !request.deepen_not.is_empty()
2045 || request.deepen_relative)
2046 {
2047 return Err(GitError::InvalidFormat(
2048 "fetch request uses shallow/deepen arguments without advertised shallow feature".into(),
2049 ));
2050 }
2051 if !features.filter && request.filter.is_some() {
2052 return Err(GitError::InvalidFormat(
2053 "fetch request uses filter without advertised filter feature".into(),
2054 ));
2055 }
2056 if !features.ref_in_want && !request.want_refs.is_empty() {
2057 return Err(GitError::InvalidFormat(
2058 "fetch request uses want-ref without advertised ref-in-want feature".into(),
2059 ));
2060 }
2061 if !features.sideband_all && request.sideband_all {
2062 return Err(GitError::InvalidFormat(
2063 "fetch request uses sideband-all without advertised sideband-all feature".into(),
2064 ));
2065 }
2066 if !features.packfile_uris && request.packfile_uris.is_some() {
2067 return Err(GitError::InvalidFormat(
2068 "fetch request uses packfile-uris without advertised packfile-uris feature".into(),
2069 ));
2070 }
2071 if !features.wait_for_done && request.wait_for_done {
2072 return Err(GitError::InvalidFormat(
2073 "fetch request uses wait-for-done without advertised wait-for-done feature".into(),
2074 ));
2075 }
2076 Ok(())
2077}
2078
2079pub fn validate_protocol_v2_fetch_command_request(
2080 handshake: &TransportHandshake,
2081 format: ObjectFormat,
2082 request: &ProtocolV2CommandRequest,
2083) -> Result<ProtocolV2FetchRequest> {
2084 validate_protocol_v2_command_request_capabilities(handshake, request)?;
2085 let fetch = ProtocolV2FetchRequest::from_command_request(format, request)?;
2086 let features = parse_protocol_v2_fetch_features(&handshake.capabilities)?
2087 .ok_or_else(|| GitError::InvalidFormat("fetch command was not advertised".into()))?;
2088 validate_protocol_v2_fetch_request_features(&features, &fetch)?;
2089 Ok(fetch)
2090}
2091
2092pub fn validate_protocol_v2_object_info_command_request(
2093 handshake: &TransportHandshake,
2094 format: ObjectFormat,
2095 request: &ProtocolV2CommandRequest,
2096) -> Result<ProtocolV2ObjectInfoRequest> {
2097 validate_protocol_v2_command_request_capabilities(handshake, request)?;
2098 let object_info = ProtocolV2ObjectInfoRequest::from_command_request(format, request)?;
2099 protocol_v2_capability(&handshake.capabilities, "object-info")
2100 .ok_or_else(|| GitError::InvalidFormat("object-info command was not advertised".into()))?;
2101 Ok(object_info)
2102}
2103
2104pub fn classify_protocol_v2_command_request(
2105 handshake: &TransportHandshake,
2106 format: ObjectFormat,
2107 request: &ProtocolV2CommandRequest,
2108) -> Result<ProtocolV2Command> {
2109 match request.command.as_str() {
2110 "ls-refs" => validate_protocol_v2_ls_refs_command_request(handshake, request)
2111 .map(ProtocolV2Command::LsRefs),
2112 "fetch" => validate_protocol_v2_fetch_command_request(handshake, format, request)
2113 .map(ProtocolV2Command::Fetch),
2114 "object-info" => {
2115 validate_protocol_v2_object_info_command_request(handshake, format, request)
2116 .map(ProtocolV2Command::ObjectInfo)
2117 }
2118 _ => {
2119 validate_protocol_v2_command_request_capabilities(handshake, request)?;
2120 Ok(ProtocolV2Command::Unknown(request.clone()))
2121 }
2122 }
2123}
2124
2125pub fn classify_protocol_v2_request(
2126 handshake: &TransportHandshake,
2127 format: ObjectFormat,
2128 request: &ProtocolV2Request,
2129) -> Result<ProtocolV2SessionRequest> {
2130 match request {
2131 ProtocolV2Request::Command(command) => {
2132 classify_protocol_v2_command_request(handshake, format, command)
2133 .map(ProtocolV2SessionRequest::Command)
2134 }
2135 ProtocolV2Request::Done => Ok(ProtocolV2SessionRequest::Done),
2136 }
2137}
2138
2139pub fn read_protocol_v2_session_request(
2140 handshake: &TransportHandshake,
2141 format: ObjectFormat,
2142 reader: &mut impl Read,
2143) -> Result<ProtocolV2SessionRequest> {
2144 let request = read_protocol_v2_request(reader)?;
2145 classify_protocol_v2_request(handshake, format, &request)
2146}
2147
2148fn protocol_v2_capability<'a>(
2149 capabilities: &'a [Capability],
2150 name: &str,
2151) -> Option<&'a Capability> {
2152 capabilities
2153 .iter()
2154 .find(|capability| capability.name == name)
2155}
2156
2157fn validate_protocol_v2_object_format_request(
2158 advertised: &Capability,
2159 requested: &Capability,
2160) -> Result<()> {
2161 let Some(advertised) = &advertised.value else {
2162 return Err(GitError::InvalidFormat(
2163 "advertised object-format capability is missing a value".into(),
2164 ));
2165 };
2166 let Some(requested) = &requested.value else {
2167 return Err(GitError::InvalidFormat(
2168 "requested object-format capability is missing a value".into(),
2169 ));
2170 };
2171 if advertised != requested {
2172 return Err(GitError::InvalidFormat(format!(
2173 "requested object-format {requested} does not match advertised {advertised}"
2174 )));
2175 }
2176 Ok(())
2177}
2178
2179fn parse_protocol_v2_ls_refs_feature_value(
2180 value: Option<&str>,
2181) -> Result<ProtocolV2LsRefsFeatures> {
2182 let mut out = ProtocolV2LsRefsFeatures::default();
2183 let Some(value) = value else {
2184 return Ok(out);
2185 };
2186 if value.is_empty() {
2187 return Err(GitError::InvalidFormat(
2188 "protocol v2 ls-refs capability value is empty".into(),
2189 ));
2190 }
2191 for feature in value.split(' ') {
2192 validate_protocol_v2_token("ls-refs feature", feature)?;
2193 match feature {
2194 "unborn" => out.unborn = true,
2195 other => out.unknown.push(other.to_string()),
2196 }
2197 }
2198 Ok(out)
2199}
2200
2201fn parse_protocol_v2_fetch_feature_value(value: Option<&str>) -> Result<ProtocolV2FetchFeatures> {
2202 let mut out = ProtocolV2FetchFeatures::default();
2203 let Some(value) = value else {
2204 return Ok(out);
2205 };
2206 if value.is_empty() {
2207 return Err(GitError::InvalidFormat(
2208 "protocol v2 fetch capability value is empty".into(),
2209 ));
2210 }
2211 for feature in value.split(' ') {
2212 validate_protocol_v2_token("fetch feature", feature)?;
2213 match feature {
2214 "shallow" => out.shallow = true,
2215 "wait-for-done" => out.wait_for_done = true,
2216 "filter" => out.filter = true,
2217 "ref-in-want" => out.ref_in_want = true,
2218 "sideband-all" => out.sideband_all = true,
2219 "packfile-uris" => out.packfile_uris = true,
2220 other => out.unknown.push(other.to_string()),
2221 }
2222 }
2223 Ok(out)
2224}
2225pub(crate) fn parse_protocol_v2_capability_line(payload: &[u8]) -> Result<Capability> {
2226 let payload = trim_trailing_lf(payload);
2227 if payload.is_empty() {
2228 return Err(GitError::InvalidFormat(
2229 "empty protocol v2 capability line".into(),
2230 ));
2231 }
2232 let text =
2233 std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
2234 let (name, value) = text
2235 .split_once('=')
2236 .map_or((text, None), |(name, value)| (name, Some(value)));
2237 validate_capability_name(name)?;
2238 if let Some(value) = value {
2239 validate_protocol_v2_capability_value(value)?;
2240 }
2241 Ok(Capability {
2242 name: name.to_string(),
2243 value: value.map(str::to_string),
2244 })
2245}
2246
2247pub(crate) fn parse_protocol_v2_command_line(payload: &[u8]) -> Result<String> {
2248 let payload = trim_trailing_lf(payload);
2249 let text =
2250 std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
2251 let Some(command) = text.strip_prefix("command=") else {
2252 return Err(GitError::InvalidFormat(
2253 "protocol v2 command request missing command prefix".into(),
2254 ));
2255 };
2256 validate_capability_name(command)?;
2257 Ok(command.to_string())
2258}
2259
2260fn parse_protocol_v2_ls_refs_line(
2261 format: ObjectFormat,
2262 payload: &[u8],
2263) -> Result<ProtocolV2LsRefsRecord> {
2264 let payload = trim_trailing_lf(payload);
2265 if payload.is_empty() {
2266 return Err(GitError::InvalidFormat(
2267 "ls-refs response line is empty".into(),
2268 ));
2269 }
2270 let text =
2271 std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
2272 let mut fields = text.split(' ');
2273 let first = fields
2274 .next()
2275 .ok_or_else(|| GitError::InvalidFormat("ls-refs response line is empty".into()))?;
2276 if first == "unborn" {
2277 let name = fields
2278 .next()
2279 .ok_or_else(|| GitError::InvalidFormat("ls-refs unborn line is missing name".into()))?;
2280 validate_protocol_v2_token("ls-refs ref name", name)?;
2281 let (symref_target, attributes) = parse_protocol_v2_ls_refs_attributes(format, fields)?;
2282 return Ok(ProtocolV2LsRefsRecord::Unborn {
2283 name: name.to_string(),
2284 symref_target,
2285 attributes,
2286 });
2287 }
2288
2289 let oid = ObjectId::from_hex(format, first)?;
2290 let name = fields
2291 .next()
2292 .ok_or_else(|| GitError::InvalidFormat("ls-refs ref line is missing name".into()))?;
2293 validate_protocol_v2_token("ls-refs ref name", name)?;
2294 let (peeled, symref_target, attributes) =
2295 parse_protocol_v2_ls_refs_ref_attributes(format, fields)?;
2296 Ok(ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
2297 oid,
2298 name: name.to_string(),
2299 peeled,
2300 symref_target,
2301 attributes,
2302 }))
2303}
2304
2305fn parse_protocol_v2_ls_refs_ref_attributes<'a>(
2306 format: ObjectFormat,
2307 fields: impl Iterator<Item = &'a str>,
2308) -> Result<(Option<ObjectId>, Option<String>, Vec<String>)> {
2309 let mut peeled = None;
2310 let (symref_target, attributes) =
2311 parse_protocol_v2_ls_refs_attributes_with(format, fields, |attr| {
2312 if let Some(value) = attr.strip_prefix("peeled:") {
2313 if peeled.is_some() {
2314 return Err(GitError::InvalidFormat(
2315 "ls-refs response has duplicate peeled attribute".into(),
2316 ));
2317 }
2318 peeled = Some(ObjectId::from_hex(format, value)?);
2319 return Ok(true);
2320 }
2321 Ok(false)
2322 })?;
2323 Ok((peeled, symref_target, attributes))
2324}
2325
2326fn parse_protocol_v2_ls_refs_attributes<'a>(
2327 format: ObjectFormat,
2328 fields: impl Iterator<Item = &'a str>,
2329) -> Result<(Option<String>, Vec<String>)> {
2330 parse_protocol_v2_ls_refs_attributes_with(format, fields, |attr| {
2331 if attr.starts_with("peeled:") {
2332 return Err(GitError::InvalidFormat(
2333 "ls-refs unborn line has peeled attribute".into(),
2334 ));
2335 }
2336 Ok(false)
2337 })
2338}
2339
2340fn parse_protocol_v2_ls_refs_attributes_with<'a, F>(
2341 _format: ObjectFormat,
2342 fields: impl Iterator<Item = &'a str>,
2343 mut handle_known: F,
2344) -> Result<(Option<String>, Vec<String>)>
2345where
2346 F: FnMut(&str) -> Result<bool>,
2347{
2348 let mut symref_target = None;
2349 let mut attributes = Vec::new();
2350 for attr in fields {
2351 validate_protocol_v2_token("ls-refs attribute", attr)?;
2352 if let Some(value) = attr.strip_prefix("symref-target:") {
2353 if symref_target.is_some() {
2354 return Err(GitError::InvalidFormat(
2355 "ls-refs response has duplicate symref-target attribute".into(),
2356 ));
2357 }
2358 validate_protocol_v2_token("ls-refs symref-target", value)?;
2359 symref_target = Some(value.to_string());
2360 } else if !handle_known(attr)? {
2361 attributes.push(attr.to_string());
2362 }
2363 }
2364 Ok((symref_target, attributes))
2365}
2366
2367fn format_protocol_v2_ls_refs_record(record: &ProtocolV2LsRefsRecord) -> Result<String> {
2368 let mut out = String::new();
2369 match record {
2370 ProtocolV2LsRefsRecord::Ref(reference) => {
2371 validate_protocol_v2_token("ls-refs ref name", &reference.name)?;
2372 out.push_str(&reference.oid.to_string());
2373 out.push(' ');
2374 out.push_str(&reference.name);
2375 if let Some(peeled) = &reference.peeled {
2376 if peeled.format() != reference.oid.format() {
2377 return Err(GitError::InvalidObjectId(
2378 "ls-refs peeled object format does not match ref object format".into(),
2379 ));
2380 }
2381 out.push(' ');
2382 out.push_str("peeled:");
2383 out.push_str(&peeled.to_string());
2384 }
2385 if let Some(target) = &reference.symref_target {
2386 validate_protocol_v2_token("ls-refs symref-target", target)?;
2387 out.push(' ');
2388 out.push_str("symref-target:");
2389 out.push_str(target);
2390 }
2391 append_protocol_v2_ls_refs_attributes(&mut out, &reference.attributes)?;
2392 }
2393 ProtocolV2LsRefsRecord::Unborn {
2394 name,
2395 symref_target,
2396 attributes,
2397 } => {
2398 validate_protocol_v2_token("ls-refs ref name", name)?;
2399 out.push_str("unborn ");
2400 out.push_str(name);
2401 if let Some(target) = symref_target {
2402 validate_protocol_v2_token("ls-refs symref-target", target)?;
2403 out.push(' ');
2404 out.push_str("symref-target:");
2405 out.push_str(target);
2406 }
2407 append_protocol_v2_ls_refs_attributes(&mut out, attributes)?;
2408 }
2409 }
2410 Ok(out)
2411}
2412
2413fn append_protocol_v2_ls_refs_attributes(out: &mut String, attributes: &[String]) -> Result<()> {
2414 for attr in attributes {
2415 validate_protocol_v2_token("ls-refs attribute", attr)?;
2416 if attr.starts_with("peeled:") || attr.starts_with("symref-target:") {
2417 return Err(GitError::InvalidFormat(
2418 "ls-refs generic attributes must not duplicate known attributes".into(),
2419 ));
2420 }
2421 out.push(' ');
2422 out.push_str(attr);
2423 }
2424 Ok(())
2425}
2426
2427fn parse_fetch_section_header(payload: &[u8]) -> Result<String> {
2428 let name = parse_protocol_v2_line_text("fetch response section", payload)?;
2429 validate_capability_name(name)?;
2430 Ok(name.to_string())
2431}
2432
2433fn flush_terminates_protocol_v2_response(frames: &[PktLineFrame], idx: usize) -> bool {
2434 idx + 1 == frames.len()
2435 || (idx + 2 == frames.len() && matches!(frames[idx + 1], PktLineFrame::ResponseEnd))
2436}
2437
2438fn parse_fetch_section(
2439 format: ObjectFormat,
2440 name: String,
2441 lines: Vec<Vec<u8>>,
2442) -> Result<ProtocolV2FetchResponseSection> {
2443 match name.as_str() {
2444 "acknowledgments" => lines
2445 .iter()
2446 .map(|line| parse_fetch_acknowledgment(format, line))
2447 .collect::<Result<Vec<_>>>()
2448 .map(ProtocolV2FetchResponseSection::Acknowledgments),
2449 "shallow-info" => lines
2450 .iter()
2451 .map(|line| parse_fetch_shallow_info(format, line))
2452 .collect::<Result<Vec<_>>>()
2453 .map(ProtocolV2FetchResponseSection::ShallowInfo),
2454 "wanted-refs" => lines
2455 .iter()
2456 .map(|line| parse_fetch_wanted_ref(format, line))
2457 .collect::<Result<Vec<_>>>()
2458 .map(ProtocolV2FetchResponseSection::WantedRefs),
2459 "packfile-uris" => lines
2460 .iter()
2461 .map(|line| parse_fetch_packfile_uri(format, line))
2462 .collect::<Result<Vec<_>>>()
2463 .map(ProtocolV2FetchResponseSection::PackfileUris),
2464 "packfile" => Ok(ProtocolV2FetchResponseSection::Packfile(lines)),
2465 _ => Ok(ProtocolV2FetchResponseSection::Unknown { name, lines }),
2466 }
2467}
2468
2469fn parse_fetch_acknowledgment(
2470 format: ObjectFormat,
2471 line: &[u8],
2472) -> Result<ProtocolV2FetchAcknowledgment> {
2473 let text = parse_protocol_v2_line_text("fetch acknowledgment", line)?;
2474 match text {
2475 "NAK" => Ok(ProtocolV2FetchAcknowledgment::Nak),
2476 "ready" => Ok(ProtocolV2FetchAcknowledgment::Ready),
2477 value if value.starts_with("ACK ") => Ok(ProtocolV2FetchAcknowledgment::Ack(
2478 parse_oid_argument(format, "fetch ACK", value, "ACK ")?,
2479 )),
2480 other => Err(GitError::InvalidFormat(format!(
2481 "unsupported fetch acknowledgment {other}"
2482 ))),
2483 }
2484}
2485
2486pub(crate) fn parse_fetch_shallow_info(
2487 format: ObjectFormat,
2488 line: &[u8],
2489) -> Result<ProtocolV2FetchShallowInfo> {
2490 let text = parse_protocol_v2_line_text("fetch shallow-info", line)?;
2491 if text.starts_with("shallow ") {
2492 return Ok(ProtocolV2FetchShallowInfo::Shallow(parse_oid_argument(
2493 format,
2494 "fetch shallow",
2495 text,
2496 "shallow ",
2497 )?));
2498 }
2499 if text.starts_with("unshallow ") {
2500 return Ok(ProtocolV2FetchShallowInfo::Unshallow(parse_oid_argument(
2501 format,
2502 "fetch unshallow",
2503 text,
2504 "unshallow ",
2505 )?));
2506 }
2507 Err(GitError::InvalidFormat(format!(
2508 "unsupported fetch shallow-info {text}"
2509 )))
2510}
2511
2512fn parse_fetch_wanted_ref(format: ObjectFormat, line: &[u8]) -> Result<ProtocolV2FetchWantedRef> {
2513 let text = parse_protocol_v2_line_text("fetch wanted-ref", line)?;
2514 let (oid, name) = text
2515 .split_once(' ')
2516 .ok_or_else(|| GitError::InvalidFormat("fetch wanted-ref is missing name".into()))?;
2517 validate_protocol_v2_token("fetch wanted-ref name", name)?;
2518 Ok(ProtocolV2FetchWantedRef {
2519 oid: ObjectId::from_hex(format, oid)?,
2520 name: name.to_string(),
2521 })
2522}
2523
2524fn parse_fetch_packfile_uri(
2525 format: ObjectFormat,
2526 line: &[u8],
2527) -> Result<ProtocolV2FetchPackfileUri> {
2528 let text = parse_protocol_v2_line_text("fetch packfile-uri", line)?;
2529 let (pack_hash, uri) = text
2530 .split_once(' ')
2531 .ok_or_else(|| GitError::InvalidFormat("fetch packfile-uri is missing uri".into()))?;
2532 validate_protocol_v2_token("fetch packfile-uri hash", pack_hash)?;
2533 validate_protocol_v2_token("fetch packfile-uri", uri)?;
2534 Ok(ProtocolV2FetchPackfileUri {
2535 pack_hash: ObjectId::from_hex(format, pack_hash)?,
2536 uri: uri.to_string(),
2537 })
2538}
2539
2540fn protocol_v2_fetch_section_name(section: &ProtocolV2FetchResponseSection) -> &str {
2541 match section {
2542 ProtocolV2FetchResponseSection::Acknowledgments(_) => "acknowledgments",
2543 ProtocolV2FetchResponseSection::ShallowInfo(_) => "shallow-info",
2544 ProtocolV2FetchResponseSection::WantedRefs(_) => "wanted-refs",
2545 ProtocolV2FetchResponseSection::PackfileUris(_) => "packfile-uris",
2546 ProtocolV2FetchResponseSection::Packfile(_) => "packfile",
2547 ProtocolV2FetchResponseSection::Unknown { name, .. } => name,
2548 }
2549}
2550
2551fn format_protocol_v2_fetch_section_lines(
2552 section: &ProtocolV2FetchResponseSection,
2553) -> Result<Vec<Vec<u8>>> {
2554 match section {
2555 ProtocolV2FetchResponseSection::Acknowledgments(acks) => acks
2556 .iter()
2557 .map(|ack| match ack {
2558 ProtocolV2FetchAcknowledgment::Nak => Ok(line_from_str("NAK")),
2559 ProtocolV2FetchAcknowledgment::Ack(oid) => Ok(line_from_str(&format!("ACK {oid}"))),
2560 ProtocolV2FetchAcknowledgment::Ready => Ok(line_from_str("ready")),
2561 })
2562 .collect(),
2563 ProtocolV2FetchResponseSection::ShallowInfo(entries) => entries
2564 .iter()
2565 .map(|entry| match entry {
2566 ProtocolV2FetchShallowInfo::Shallow(oid) => {
2571 Ok(format!("shallow {oid}").into_bytes())
2572 }
2573 ProtocolV2FetchShallowInfo::Unshallow(oid) => {
2574 Ok(format!("unshallow {oid}").into_bytes())
2575 }
2576 })
2577 .collect(),
2578 ProtocolV2FetchResponseSection::WantedRefs(refs) => refs
2579 .iter()
2580 .map(|wanted| {
2581 validate_protocol_v2_token("fetch wanted-ref name", &wanted.name)?;
2582 Ok(line_from_str(&format!("{} {}", wanted.oid, wanted.name)))
2583 })
2584 .collect(),
2585 ProtocolV2FetchResponseSection::PackfileUris(uris) => uris
2586 .iter()
2587 .map(|packfile_uri| {
2588 validate_protocol_v2_token("fetch packfile-uri", &packfile_uri.uri)?;
2589 Ok(line_from_str(&format!(
2590 "{} {}",
2591 packfile_uri.pack_hash, packfile_uri.uri
2592 )))
2593 })
2594 .collect(),
2595 ProtocolV2FetchResponseSection::Packfile(lines) => Ok(lines.clone()),
2596 ProtocolV2FetchResponseSection::Unknown { name, lines } => {
2597 validate_capability_name(name)?;
2598 for line in lines {
2599 validate_protocol_v2_line("fetch unknown section line", line)?;
2600 }
2601 Ok(lines.clone())
2602 }
2603 }
2604}
2605
2606fn parse_protocol_v2_object_info_record(
2607 format: ObjectFormat,
2608 line: &[u8],
2609) -> Result<ProtocolV2ObjectInfoRecord> {
2610 let text = parse_protocol_v2_line_text("object-info record", line)?;
2611 let mut fields = text.split(' ');
2612 let oid = fields
2613 .next()
2614 .ok_or_else(|| GitError::InvalidFormat("object-info record is missing oid".into()))?;
2615 let size = fields
2616 .next()
2617 .ok_or_else(|| GitError::InvalidFormat("object-info record is missing size".into()))?;
2618 if fields.next().is_some() {
2619 return Err(GitError::InvalidFormat(
2620 "object-info record has too many fields".into(),
2621 ));
2622 }
2623 validate_protocol_v2_token("object-info oid", oid)?;
2624 validate_protocol_v2_token("object-info size", size)?;
2625 let size = size
2626 .parse::<u64>()
2627 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
2628 Ok(ProtocolV2ObjectInfoRecord {
2629 oid: ObjectId::from_hex(format, oid)?,
2630 size,
2631 })
2632}
2633
2634pub(crate) fn encode_protocol_v2_capability(capability: &Capability) -> Result<Vec<u8>> {
2635 validate_capability_name(&capability.name)?;
2636 let mut out = capability.name.as_bytes().to_vec();
2637 if let Some(value) = &capability.value {
2638 validate_protocol_v2_capability_value(value)?;
2639 out.push(b'=');
2640 out.extend_from_slice(value.as_bytes());
2641 }
2642 Ok(out)
2643}
2644
2645pub(crate) fn validate_protocol_v2_capability_value(value: &str) -> Result<()> {
2646 if value.is_empty() {
2647 return Err(GitError::InvalidFormat(
2648 "protocol v2 capability value is empty".into(),
2649 ));
2650 }
2651 if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
2652 return Err(GitError::InvalidFormat(
2653 "protocol v2 capability value contains a delimiter byte".into(),
2654 ));
2655 }
2656 Ok(())
2657}
2658
2659fn validate_protocol_v2_argument(value: &[u8]) -> Result<()> {
2660 if value.is_empty() {
2661 return Err(GitError::InvalidFormat(
2662 "protocol v2 command argument is empty".into(),
2663 ));
2664 }
2665 if value.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
2666 return Err(GitError::InvalidFormat(
2667 "protocol v2 command argument contains a delimiter byte".into(),
2668 ));
2669 }
2670 Ok(())
2671}
2672
2673pub(crate) fn parse_u32_argument(label: &str, value: &str, prefix: &str) -> Result<u32> {
2674 let number = value
2675 .strip_prefix(prefix)
2676 .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
2677 validate_protocol_v2_token(label, number)?;
2678 let parsed = number
2679 .parse::<u32>()
2680 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
2681 if parsed == 0 {
2682 return Err(GitError::InvalidFormat(format!("{label} must be positive")));
2683 }
2684 Ok(parsed)
2685}
2686
2687pub(crate) fn parse_u64_argument(label: &str, value: &str, prefix: &str) -> Result<u64> {
2688 let number = value
2689 .strip_prefix(prefix)
2690 .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
2691 validate_protocol_v2_token(label, number)?;
2692 number
2693 .parse::<u64>()
2694 .map_err(|err| GitError::InvalidFormat(err.to_string()))
2695}