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