1use sley_core::{Capability, GitError, ObjectFormat, ObjectId, Result};
2use std::io::{Read, Write};
3
4use crate::pktline::{
5 PktLineFrame, PushSourceRef, RefSpec, line_from_str, parse_pkt_line_frames_until_flush_from,
6 parse_protocol_v2_line_text, read_pkt_line_frames_until_flush, trim_trailing_lf,
7 validate_capability_field, validate_protocol_v2_token, validate_refspec_endpoint,
8 validate_refspec_shape, write_pkt_line_payload,
9};
10use crate::v0::{RefAdvertisement, encode_capabilities, parse_capabilities};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct ReceivePackCommand {
14 pub old_id: ObjectId,
15 pub new_id: ObjectId,
16 pub name: String,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Default)]
20pub struct ReceivePackRequest {
21 pub shallow: Vec<ObjectId>,
22 pub commands: Vec<ReceivePackCommand>,
23 pub capabilities: Vec<Capability>,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Default)]
27pub struct ReceivePackPushRequest {
28 pub commands: ReceivePackRequest,
29 pub push_options: Option<Vec<String>>,
30 pub packfile: Vec<u8>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Default)]
34pub struct ReceivePackPushRequestHeader {
35 pub commands: ReceivePackRequest,
36 pub push_options: Option<Vec<String>>,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Default)]
40pub struct ReceivePackPushRequestOptions {
41 pub report_status: bool,
42 pub report_status_v2: bool,
43 pub atomic: bool,
44 pub ofs_delta: bool,
45 pub side_band_64k: bool,
46 pub quiet: bool,
47 pub agent: Option<String>,
48 pub object_format: Option<ObjectFormat>,
49 pub push_options: Vec<String>,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Default)]
53pub struct ReceivePackFeatures {
54 pub report_status: bool,
55 pub report_status_v2: bool,
56 pub delete_refs: bool,
57 pub ofs_delta: bool,
58 pub atomic: bool,
59 pub push_options: bool,
60 pub side_band_64k: bool,
61 pub quiet: bool,
62 pub no_thin: bool,
63 pub agent: Option<String>,
64 pub object_format: Option<ObjectFormat>,
65 pub unknown: Vec<Capability>,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum ReceivePackUnpackStatus {
70 Ok,
71 Error(String),
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum ReceivePackCommandStatus {
76 Ok { name: String },
77 Ng { name: String, message: String },
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct ReceivePackReportStatus {
82 pub unpack: ReceivePackUnpackStatus,
83 pub commands: Vec<ReceivePackCommandStatus>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Default)]
87pub struct ReceivePackCommandStatusV2Options {
88 pub refname: Option<String>,
89 pub old_oid: Option<ObjectId>,
90 pub new_oid: Option<ObjectId>,
91 pub forced_update: bool,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum ReceivePackCommandStatusV2 {
96 Ok {
97 name: String,
98 options: ReceivePackCommandStatusV2Options,
99 },
100 Ng {
101 name: String,
102 message: String,
103 },
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct ReceivePackReportStatusV2 {
108 pub unpack: ReceivePackUnpackStatus,
109 pub commands: Vec<ReceivePackCommandStatusV2>,
110}
111
112pub fn plan_push_commands(
113 format: ObjectFormat,
114 local_refs: &[PushSourceRef],
115 remote_refs: &[RefAdvertisement],
116 refspecs: &[RefSpec],
117) -> Result<Vec<ReceivePackCommand>> {
118 let zero = zero_object_id(format)?;
119 let mut commands = Vec::new();
120 for refspec in refspecs {
121 validate_refspec_shape(refspec)?;
122 if refspec.negative {
123 return Err(GitError::InvalidFormat(
124 "push refspec must not be negative".into(),
125 ));
126 }
127 match (refspec.src.as_deref(), refspec.dst.as_deref()) {
128 (None, None) => {
129 for local in local_refs {
135 if !local.name.starts_with("refs/") {
136 continue;
137 }
138 validate_push_source_ref(format, local)?;
139 if let Some(remote) = remote_ref(remote_refs, &local.name) {
140 commands.push(ReceivePackCommand {
141 old_id: remote.oid,
142 new_id: local.oid,
143 name: local.name.clone(),
144 });
145 }
146 }
147 }
148 (None, Some(dst)) => {
149 validate_refspec_endpoint("push destination", dst)?;
150 let old_id = remote_ref(remote_refs, dst)
151 .map(|reference| reference.oid)
152 .unwrap_or_else(|| zero.clone());
153 commands.push(ReceivePackCommand {
154 old_id,
155 new_id: zero.clone(),
156 name: dst.to_string(),
157 });
158 }
159 (Some(src), dst) if refspec.pattern => {
160 let Some((src_prefix, src_suffix)) = src.split_once('*') else {
161 return Err(GitError::InvalidFormat(
162 "pattern push refspec source is missing wildcard".into(),
163 ));
164 };
165 let dst = dst.ok_or_else(|| {
166 GitError::InvalidFormat("pattern push refspec is missing destination".into())
167 })?;
168 let (dst_prefix, dst_suffix) = dst.split_once('*').ok_or_else(|| {
169 GitError::InvalidFormat(
170 "pattern push refspec destination is missing wildcard".into(),
171 )
172 })?;
173 for local in local_refs {
174 validate_push_source_ref(format, local)?;
175 let Some(middle) = local
176 .name
177 .strip_prefix(src_prefix)
178 .and_then(|value| value.strip_suffix(src_suffix))
179 else {
180 continue;
181 };
182 let name = format!("{dst_prefix}{middle}{dst_suffix}");
183 let old_id = remote_ref(remote_refs, &name)
184 .map(|reference| reference.oid)
185 .unwrap_or_else(|| zero.clone());
186 commands.push(ReceivePackCommand {
187 old_id,
188 new_id: local.oid,
189 name,
190 });
191 }
192 }
193 (Some(src), dst) => {
194 validate_refspec_endpoint("push source", src)?;
195 let local = local_ref(local_refs, src)
196 .ok_or_else(|| GitError::reference_not_found(format!("local ref {src}")))?;
197 validate_push_source_ref(format, local)?;
198 let name = dst.unwrap_or(src);
199 validate_refspec_endpoint("push destination", name)?;
200 let old_id = remote_ref(remote_refs, name)
201 .map(|reference| reference.oid)
202 .unwrap_or_else(|| zero.clone());
203 commands.push(ReceivePackCommand {
204 old_id,
205 new_id: local.oid,
206 name: name.to_string(),
207 });
208 }
209 }
210 }
211 Ok(commands)
212}
213
214pub fn build_receive_pack_push_request(
215 features: &ReceivePackFeatures,
216 commands: Vec<ReceivePackCommand>,
217 packfile: Vec<u8>,
218 options: ReceivePackPushRequestOptions,
219) -> Result<ReceivePackPushRequest> {
220 let header = build_receive_pack_push_request_header(features, commands, options)?;
221 let request = ReceivePackPushRequest {
222 commands: header.commands,
223 push_options: header.push_options,
224 packfile,
225 };
226 validate_receive_pack_push_request_features(features, &request)?;
227 Ok(request)
228}
229
230pub fn build_receive_pack_push_request_header(
231 features: &ReceivePackFeatures,
232 commands: Vec<ReceivePackCommand>,
233 options: ReceivePackPushRequestOptions,
234) -> Result<ReceivePackPushRequestHeader> {
235 let mut capabilities = Vec::new();
236 if options.report_status_v2 {
237 require_receive_pack_feature(features.report_status_v2, "report-status-v2")?;
238 capabilities.push(Capability {
239 name: "report-status-v2".into(),
240 value: None,
241 });
242 } else if options.report_status {
243 require_receive_pack_feature(features.report_status, "report-status")?;
244 capabilities.push(Capability {
245 name: "report-status".into(),
246 value: None,
247 });
248 }
249 if commands.iter().any(is_receive_pack_delete_command) {
250 require_receive_pack_feature(features.delete_refs, "delete-refs")?;
251 capabilities.push(Capability {
252 name: "delete-refs".into(),
253 value: None,
254 });
255 }
256 if options.atomic {
257 require_receive_pack_feature(features.atomic, "atomic")?;
258 capabilities.push(Capability {
259 name: "atomic".into(),
260 value: None,
261 });
262 }
263 if options.ofs_delta {
264 require_receive_pack_feature(features.ofs_delta, "ofs-delta")?;
265 capabilities.push(Capability {
266 name: "ofs-delta".into(),
267 value: None,
268 });
269 }
270 if options.side_band_64k {
271 require_receive_pack_feature(features.side_band_64k, "side-band-64k")?;
272 capabilities.push(Capability {
273 name: "side-band-64k".into(),
274 value: None,
275 });
276 }
277 if options.quiet {
278 require_receive_pack_feature(features.quiet, "quiet")?;
279 capabilities.push(Capability {
280 name: "quiet".into(),
281 value: None,
282 });
283 }
284 if let Some(agent) = &options.agent {
285 validate_capability_field("receive-pack request agent", agent)?;
286 capabilities.push(Capability {
287 name: "agent".into(),
288 value: Some(agent.clone()),
289 });
290 }
291 if let Some(format) = options.object_format {
292 if features.object_format != Some(format) {
293 return Err(GitError::InvalidFormat(
294 "receive-pack request object-format was not advertised".into(),
295 ));
296 }
297 capabilities.push(Capability {
298 name: "object-format".into(),
299 value: Some(format.name().into()),
300 });
301 }
302 let push_options = if options.push_options.is_empty() {
303 None
304 } else {
305 require_receive_pack_feature(features.push_options, "push-options")?;
306 for option in &options.push_options {
307 validate_receive_pack_push_option(option.as_bytes())?;
308 }
309 capabilities.push(Capability {
310 name: "push-options".into(),
311 value: None,
312 });
313 Some(options.push_options)
314 };
315 let header = ReceivePackPushRequestHeader {
316 commands: ReceivePackRequest {
317 commands,
318 capabilities,
319 shallow: Vec::new(),
320 },
321 push_options,
322 };
323 validate_receive_pack_push_request_features(
324 features,
325 &ReceivePackPushRequest {
326 commands: header.commands.clone(),
327 push_options: header.push_options.clone(),
328 packfile: Vec::new(),
329 },
330 )?;
331 Ok(header)
332}
333pub fn parse_receive_pack_request(
334 format: ObjectFormat,
335 frames: &[PktLineFrame],
336) -> Result<ReceivePackRequest> {
337 let mut request = ReceivePackRequest::default();
338 let mut saw_command = false;
339 let mut saw_flush = false;
340 for (idx, frame) in frames.iter().enumerate() {
341 match frame {
342 PktLineFrame::Data(payload) if !saw_flush => {
343 let payload = trim_trailing_lf(payload);
344 if payload.is_empty() {
345 return Err(GitError::InvalidFormat(
346 "receive-pack request line is empty".into(),
347 ));
348 }
349 if let Some(shallow) = payload.strip_prefix(b"shallow ") {
350 if saw_command {
351 return Err(GitError::InvalidFormat(
352 "receive-pack request has shallow after commands".into(),
353 ));
354 }
355 let shallow = std::str::from_utf8(shallow)
356 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
357 validate_protocol_v2_token("receive-pack shallow", shallow)?;
358 request.shallow.push(ObjectId::from_hex(format, shallow)?);
359 continue;
360 }
361
362 let (command, capabilities) = match payload.iter().position(|byte| *byte == 0) {
363 Some(nul) if !saw_command => (
364 &payload[..nul],
365 Some(parse_capabilities(&payload[nul + 1..])?),
366 ),
367 Some(_) => {
368 return Err(GitError::InvalidFormat(
369 "receive-pack capabilities must appear on the first command".into(),
370 ));
371 }
372 None => (payload, None),
373 };
374 let command = parse_receive_pack_command(format, command)?;
375 if let Some(capabilities) = capabilities {
376 request.capabilities = capabilities;
377 }
378 request.commands.push(command);
379 saw_command = true;
380 }
381 PktLineFrame::Data(_) => {
382 return Err(GitError::InvalidFormat(
383 "receive-pack request has data after flush".into(),
384 ));
385 }
386 PktLineFrame::Flush => {
387 saw_flush = true;
388 if idx + 1 != frames.len() {
389 return Err(GitError::InvalidFormat(
390 "receive-pack request has frames after flush".into(),
391 ));
392 }
393 }
394 PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
395 return Err(GitError::InvalidFormat(
396 "receive-pack request contains a non-flush control packet".into(),
397 ));
398 }
399 }
400 }
401 if !saw_flush {
402 return Err(GitError::InvalidFormat(
403 "receive-pack request missing flush".into(),
404 ));
405 }
406 if !request.shallow.is_empty() && request.commands.is_empty() {
407 return Err(GitError::InvalidFormat(
408 "receive-pack request has shallow lines without commands".into(),
409 ));
410 }
411 Ok(request)
412}
413
414pub fn encode_receive_pack_request(request: &ReceivePackRequest) -> Result<Vec<PktLineFrame>> {
415 if !request.shallow.is_empty() && request.commands.is_empty() {
416 return Err(GitError::InvalidFormat(
417 "receive-pack request has shallow lines without commands".into(),
418 ));
419 }
420
421 let mut frames = Vec::new();
422 for oid in &request.shallow {
423 frames.push(PktLineFrame::data(line_from_str(&format!(
424 "shallow {oid}"
425 )))?);
426 }
427 for (idx, command) in request.commands.iter().enumerate() {
428 let mut payload = format_receive_pack_command(command)?;
429 if idx == 0 && !request.capabilities.is_empty() {
430 payload.push(0);
431 payload.extend_from_slice(&encode_capabilities(&request.capabilities)?);
432 }
433 payload.push(b'\n');
434 frames.push(PktLineFrame::data(payload)?);
435 }
436 frames.push(PktLineFrame::Flush);
437 Ok(frames)
438}
439
440pub fn read_receive_pack_request(
441 format: ObjectFormat,
442 reader: &mut impl Read,
443) -> Result<ReceivePackRequest> {
444 let frames = read_pkt_line_frames_until_flush(reader)?;
445 parse_receive_pack_request(format, &frames)
446}
447
448pub fn write_receive_pack_request(
449 writer: &mut impl Write,
450 request: &ReceivePackRequest,
451) -> Result<()> {
452 if !request.shallow.is_empty() && request.commands.is_empty() {
453 return Err(GitError::InvalidFormat(
454 "receive-pack request has shallow lines without commands".into(),
455 ));
456 }
457
458 for oid in &request.shallow {
459 write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
460 }
461 for (idx, command) in request.commands.iter().enumerate() {
462 let mut payload = format_receive_pack_command(command)?;
463 if idx == 0 && !request.capabilities.is_empty() {
464 payload.push(0);
465 payload.extend_from_slice(&encode_capabilities(&request.capabilities)?);
466 }
467 payload.push(b'\n');
468 write_pkt_line_payload(writer, &payload)?;
469 }
470 writer.write_all(b"0000")?;
471 Ok(())
472}
473
474pub fn parse_receive_pack_push_request(
475 format: ObjectFormat,
476 input: &[u8],
477 has_push_options: bool,
478) -> Result<ReceivePackPushRequest> {
479 let (command_frames, consumed) = parse_pkt_line_frames_until_flush_from(input)?;
480 let commands = parse_receive_pack_request(format, &command_frames)?;
481 let mut offset = consumed;
482 let push_options = if has_push_options {
483 let (push_option_frames, consumed) =
484 parse_pkt_line_frames_until_flush_from(&input[offset..])?;
485 offset += consumed;
486 Some(parse_receive_pack_push_options(&push_option_frames)?)
487 } else {
488 None
489 };
490 Ok(ReceivePackPushRequest {
491 commands,
492 push_options,
493 packfile: input[offset..].to_vec(),
494 })
495}
496
497pub fn encode_receive_pack_push_request(request: &ReceivePackPushRequest) -> Result<Vec<u8>> {
498 let mut out = Vec::new();
499 write_receive_pack_request(&mut out, &request.commands)?;
500 if let Some(push_options) = &request.push_options {
501 write_receive_pack_push_options(&mut out, push_options)?;
502 }
503 out.extend_from_slice(&request.packfile);
504 Ok(out)
505}
506
507pub fn read_receive_pack_push_request(
508 format: ObjectFormat,
509 reader: &mut impl Read,
510 has_push_options: bool,
511) -> Result<ReceivePackPushRequest> {
512 let header = read_receive_pack_push_request_header(format, reader, has_push_options)?;
513 let mut packfile = Vec::new();
514 reader.read_to_end(&mut packfile)?;
515 Ok(ReceivePackPushRequest {
516 commands: header.commands,
517 push_options: header.push_options,
518 packfile,
519 })
520}
521
522pub fn read_receive_pack_push_request_header(
523 format: ObjectFormat,
524 reader: &mut impl Read,
525 has_push_options: bool,
526) -> Result<ReceivePackPushRequestHeader> {
527 let commands = read_receive_pack_request(format, reader)?;
528 let push_options = if has_push_options {
529 Some(read_receive_pack_push_options(reader)?)
530 } else {
531 None
532 };
533 Ok(ReceivePackPushRequestHeader {
534 commands,
535 push_options,
536 })
537}
538
539pub fn write_receive_pack_push_request(
540 writer: &mut impl Write,
541 request: &ReceivePackPushRequest,
542) -> Result<()> {
543 write_receive_pack_push_request_header(
544 writer,
545 &ReceivePackPushRequestHeader {
546 commands: request.commands.clone(),
547 push_options: request.push_options.clone(),
548 },
549 )?;
550 writer.write_all(&request.packfile)?;
551 Ok(())
552}
553
554pub fn write_receive_pack_push_request_header(
555 writer: &mut impl Write,
556 header: &ReceivePackPushRequestHeader,
557) -> Result<()> {
558 write_receive_pack_request(writer, &header.commands)?;
559 if let Some(push_options) = &header.push_options {
560 write_receive_pack_push_options(writer, push_options)?;
561 }
562 Ok(())
563}
564
565pub fn parse_receive_pack_features(capabilities: &[Capability]) -> Result<ReceivePackFeatures> {
566 let mut features = ReceivePackFeatures::default();
567 for capability in capabilities {
568 match capability.name.as_str() {
569 "report-status" => {
570 reject_capability_value("receive-pack report-status", capability)?;
571 if features.report_status {
572 return Err(GitError::InvalidFormat(
573 "receive-pack has duplicate report-status capability".into(),
574 ));
575 }
576 features.report_status = true;
577 }
578 "report-status-v2" => {
579 reject_capability_value("receive-pack report-status-v2", capability)?;
580 if features.report_status_v2 {
581 return Err(GitError::InvalidFormat(
582 "receive-pack has duplicate report-status-v2 capability".into(),
583 ));
584 }
585 features.report_status_v2 = true;
586 }
587 "delete-refs" => {
588 reject_capability_value("receive-pack delete-refs", capability)?;
589 if features.delete_refs {
590 return Err(GitError::InvalidFormat(
591 "receive-pack has duplicate delete-refs capability".into(),
592 ));
593 }
594 features.delete_refs = true;
595 }
596 "ofs-delta" => {
597 reject_capability_value("receive-pack ofs-delta", capability)?;
598 if features.ofs_delta {
599 return Err(GitError::InvalidFormat(
600 "receive-pack has duplicate ofs-delta capability".into(),
601 ));
602 }
603 features.ofs_delta = true;
604 }
605 "atomic" => {
606 reject_capability_value("receive-pack atomic", capability)?;
607 if features.atomic {
608 return Err(GitError::InvalidFormat(
609 "receive-pack has duplicate atomic capability".into(),
610 ));
611 }
612 features.atomic = true;
613 }
614 "push-options" => {
615 reject_capability_value("receive-pack push-options", capability)?;
616 if features.push_options {
617 return Err(GitError::InvalidFormat(
618 "receive-pack has duplicate push-options capability".into(),
619 ));
620 }
621 features.push_options = true;
622 }
623 "side-band-64k" => {
624 reject_capability_value("receive-pack side-band-64k", capability)?;
625 if features.side_band_64k {
626 return Err(GitError::InvalidFormat(
627 "receive-pack has duplicate side-band-64k capability".into(),
628 ));
629 }
630 features.side_band_64k = true;
631 }
632 "quiet" => {
633 reject_capability_value("receive-pack quiet", capability)?;
634 if features.quiet {
635 return Err(GitError::InvalidFormat(
636 "receive-pack has duplicate quiet capability".into(),
637 ));
638 }
639 features.quiet = true;
640 }
641 "no-thin" => {
642 reject_capability_value("receive-pack no-thin", capability)?;
643 if features.no_thin {
644 return Err(GitError::InvalidFormat(
645 "receive-pack has duplicate no-thin capability".into(),
646 ));
647 }
648 features.no_thin = true;
649 }
650 "agent" => {
651 let Some(agent) = &capability.value else {
652 return Err(GitError::InvalidFormat(
653 "receive-pack agent capability is missing value".into(),
654 ));
655 };
656 if features.agent.is_some() {
657 return Err(GitError::InvalidFormat(
658 "receive-pack has duplicate agent capability".into(),
659 ));
660 }
661 validate_capability_field("receive-pack agent", agent)?;
662 features.agent = Some(agent.clone());
663 }
664 "object-format" => {
665 let Some(format) = &capability.value else {
666 return Err(GitError::InvalidFormat(
667 "receive-pack object-format capability is missing value".into(),
668 ));
669 };
670 if features.object_format.is_some() {
671 return Err(GitError::InvalidFormat(
672 "receive-pack has duplicate object-format capability".into(),
673 ));
674 }
675 validate_capability_field("receive-pack object-format", format)?;
676 features.object_format = Some(format.parse()?);
677 }
678 _ => {
679 encode_capabilities(std::slice::from_ref(capability))?;
680 if features
681 .unknown
682 .iter()
683 .any(|known| known.name == capability.name)
684 {
685 return Err(GitError::InvalidFormat(format!(
686 "receive-pack has duplicate {} capability",
687 capability.name
688 )));
689 }
690 features.unknown.push(capability.clone());
691 }
692 }
693 }
694 Ok(features)
695}
696
697pub fn encode_receive_pack_features(features: &ReceivePackFeatures) -> Result<Vec<Capability>> {
698 let mut capabilities = Vec::new();
699 if features.report_status {
700 capabilities.push(Capability {
701 name: "report-status".into(),
702 value: None,
703 });
704 }
705 if features.report_status_v2 {
706 capabilities.push(Capability {
707 name: "report-status-v2".into(),
708 value: None,
709 });
710 }
711 if features.delete_refs {
712 capabilities.push(Capability {
713 name: "delete-refs".into(),
714 value: None,
715 });
716 }
717 if features.ofs_delta {
718 capabilities.push(Capability {
719 name: "ofs-delta".into(),
720 value: None,
721 });
722 }
723 if features.atomic {
724 capabilities.push(Capability {
725 name: "atomic".into(),
726 value: None,
727 });
728 }
729 if features.push_options {
730 capabilities.push(Capability {
731 name: "push-options".into(),
732 value: None,
733 });
734 }
735 if features.side_band_64k {
736 capabilities.push(Capability {
737 name: "side-band-64k".into(),
738 value: None,
739 });
740 }
741 if features.quiet {
742 capabilities.push(Capability {
743 name: "quiet".into(),
744 value: None,
745 });
746 }
747 if features.no_thin {
748 capabilities.push(Capability {
749 name: "no-thin".into(),
750 value: None,
751 });
752 }
753 if let Some(agent) = &features.agent {
754 validate_capability_field("receive-pack agent", agent)?;
755 capabilities.push(Capability {
756 name: "agent".into(),
757 value: Some(agent.clone()),
758 });
759 }
760 if let Some(format) = features.object_format {
761 capabilities.push(Capability {
762 name: "object-format".into(),
763 value: Some(format.name().into()),
764 });
765 }
766 for capability in &features.unknown {
767 if is_known_receive_pack_capability(&capability.name) {
768 return Err(GitError::InvalidFormat(format!(
769 "receive-pack unknown capability duplicates known capability {}",
770 capability.name
771 )));
772 }
773 encode_capabilities(std::slice::from_ref(capability))?;
774 capabilities.push(capability.clone());
775 }
776 Ok(capabilities)
777}
778
779pub fn validate_receive_pack_push_request_features(
780 features: &ReceivePackFeatures,
781 request: &ReceivePackPushRequest,
782) -> Result<()> {
783 for capability in &request.commands.capabilities {
784 if matches!(
785 capability.name.as_str(),
786 "report-status"
787 | "report-status-v2"
788 | "delete-refs"
789 | "ofs-delta"
790 | "atomic"
791 | "push-options"
792 | "side-band-64k"
793 | "quiet"
794 | "no-thin"
795 ) {
796 reject_capability_value("receive-pack request capability", capability)?;
797 }
798 match capability.name.as_str() {
799 "report-status" if !features.report_status => {
800 return Err(GitError::InvalidFormat(
801 "receive-pack request uses report-status without advertised capability".into(),
802 ));
803 }
804 "report-status-v2" if !features.report_status_v2 => {
805 return Err(GitError::InvalidFormat(
806 "receive-pack request uses report-status-v2 without advertised capability"
807 .into(),
808 ));
809 }
810 "delete-refs" if !features.delete_refs => {
811 return Err(GitError::InvalidFormat(
812 "receive-pack request uses delete-refs without advertised capability".into(),
813 ));
814 }
815 "ofs-delta" if !features.ofs_delta => {
816 return Err(GitError::InvalidFormat(
817 "receive-pack request uses ofs-delta without advertised capability".into(),
818 ));
819 }
820 "atomic" if !features.atomic => {
821 return Err(GitError::InvalidFormat(
822 "receive-pack request uses atomic without advertised capability".into(),
823 ));
824 }
825 "push-options" if !features.push_options => {
826 return Err(GitError::InvalidFormat(
827 "receive-pack request uses push-options without advertised capability".into(),
828 ));
829 }
830 "side-band-64k" if !features.side_band_64k => {
831 return Err(GitError::InvalidFormat(
832 "receive-pack request uses side-band-64k without advertised capability".into(),
833 ));
834 }
835 "quiet" if !features.quiet => {
836 return Err(GitError::InvalidFormat(
837 "receive-pack request uses quiet without advertised capability".into(),
838 ));
839 }
840 "no-thin" => {
841 return Err(GitError::InvalidFormat(
842 "receive-pack request must not request no-thin".into(),
843 ));
844 }
845 "agent" => {
846 validate_capability_field(
847 "receive-pack request agent",
848 capability.value.as_deref().unwrap_or_default(),
849 )?;
850 }
851 "object-format" => {
852 let Some(value) = &capability.value else {
853 return Err(GitError::InvalidFormat(
854 "receive-pack request object-format capability is missing value".into(),
855 ));
856 };
857 let requested_format: ObjectFormat = value.parse()?;
858 if features.object_format != Some(requested_format) {
859 return Err(GitError::InvalidFormat(
860 "receive-pack request object-format was not advertised".into(),
861 ));
862 }
863 }
864 name if is_known_receive_pack_capability(name) => {}
865 _ => {
866 if !features
867 .unknown
868 .iter()
869 .any(|advertised| advertised.name == capability.name)
870 {
871 return Err(GitError::InvalidFormat(format!(
872 "receive-pack request uses unadvertised capability {}",
873 capability.name
874 )));
875 }
876 }
877 }
878 }
879
880 let requested_push_options = request
881 .commands
882 .capabilities
883 .iter()
884 .any(|capability| capability.name == "push-options");
885 match (requested_push_options, &request.push_options) {
886 (true, Some(_)) => {}
887 (true, None) => {
888 return Err(GitError::InvalidFormat(
889 "receive-pack request uses push-options without push-options section".into(),
890 ));
891 }
892 (false, Some(_)) => {
893 return Err(GitError::InvalidFormat(
894 "receive-pack request has push-options section without requested capability".into(),
895 ));
896 }
897 (false, None) => {}
898 }
899
900 let has_delete = request
901 .commands
902 .commands
903 .iter()
904 .any(is_receive_pack_delete_command);
905 if has_delete && !features.delete_refs {
906 return Err(GitError::InvalidFormat(
907 "receive-pack request deletes refs without advertised delete-refs capability".into(),
908 ));
909 }
910
911 let has_update_or_create = request
912 .commands
913 .commands
914 .iter()
915 .any(|command| !is_receive_pack_delete_command(command));
916 if !has_update_or_create && !request.packfile.is_empty() {
917 return Err(GitError::InvalidFormat(
918 "receive-pack delete-only request must not include packfile".into(),
919 ));
920 }
921 Ok(())
922}
923
924pub fn apply_receive_pack_push_request<R, I, C, U, D>(
925 features: &ReceivePackFeatures,
926 request: &ReceivePackPushRequest,
927 mut read_ref: R,
928 mut install_pack: I,
929 mut contains_object: C,
930 mut apply_updates: U,
931 mut delete_ref: D,
932) -> Result<ReceivePackReportStatus>
933where
934 R: FnMut(&str) -> Result<Option<ObjectId>>,
935 I: FnMut(&[u8]) -> Result<()>,
936 C: FnMut(&ObjectId) -> Result<bool>,
937 U: FnMut(&[ReceivePackCommand]) -> Result<()>,
938 D: FnMut(&ReceivePackCommand) -> Result<()>,
939{
940 validate_receive_pack_push_request_features(features, request)?;
941
942 for command in request
943 .commands
944 .commands
945 .iter()
946 .filter(|command| is_receive_pack_delete_command(command))
947 {
948 if !command.old_id.is_null() && read_ref(&command.name)? != Some(command.old_id.clone()) {
949 return Err(GitError::Transaction(format!(
950 "expected ref {} to match",
951 command.name
952 )));
953 }
954 }
955
956 let updates = request
957 .commands
958 .commands
959 .iter()
960 .filter(|command| !is_receive_pack_delete_command(command))
961 .cloned()
962 .collect::<Vec<_>>();
963 if !updates.is_empty() {
964 if !request.packfile.is_empty() {
965 install_pack(&request.packfile)?;
966 }
967 for command in &updates {
968 if !contains_object(&command.new_id)? {
969 return Err(GitError::InvalidObject(format!(
970 "receive-pack packfile did not provide {}",
971 command.new_id
972 )));
973 }
974 }
975 apply_updates(&updates)?;
976 }
977
978 for command in request
979 .commands
980 .commands
981 .iter()
982 .filter(|command| is_receive_pack_delete_command(command))
983 {
984 delete_ref(command)?;
985 }
986
987 Ok(ReceivePackReportStatus {
988 unpack: ReceivePackUnpackStatus::Ok,
989 commands: request
990 .commands
991 .commands
992 .iter()
993 .map(|command| ReceivePackCommandStatus::Ok {
994 name: command.name.clone(),
995 })
996 .collect(),
997 })
998}
999
1000pub fn parse_receive_pack_report_status(
1001 frames: &[PktLineFrame],
1002) -> Result<ReceivePackReportStatus> {
1003 let Some((first, rest)) = frames.split_first() else {
1004 return Err(GitError::InvalidFormat(
1005 "receive-pack report-status is empty".into(),
1006 ));
1007 };
1008 let PktLineFrame::Data(payload) = first else {
1009 return Err(GitError::InvalidFormat(
1010 "receive-pack report-status must start with unpack status".into(),
1011 ));
1012 };
1013 let unpack = parse_receive_pack_unpack_status(payload)?;
1014
1015 let mut commands = Vec::new();
1016 let mut saw_flush = false;
1017 for (idx, frame) in rest.iter().enumerate() {
1018 match frame {
1019 PktLineFrame::Data(payload) if !saw_flush => {
1020 commands.push(parse_receive_pack_command_status(payload)?);
1021 }
1022 PktLineFrame::Data(_) => {
1023 return Err(GitError::InvalidFormat(
1024 "receive-pack report-status has data after flush".into(),
1025 ));
1026 }
1027 PktLineFrame::Flush => {
1028 saw_flush = true;
1029 if idx + 1 != rest.len() {
1030 return Err(GitError::InvalidFormat(
1031 "receive-pack report-status has frames after flush".into(),
1032 ));
1033 }
1034 }
1035 PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
1036 return Err(GitError::InvalidFormat(
1037 "receive-pack report-status contains a non-flush control packet".into(),
1038 ));
1039 }
1040 }
1041 }
1042 if !saw_flush {
1043 return Err(GitError::InvalidFormat(
1044 "receive-pack report-status missing flush".into(),
1045 ));
1046 }
1047 Ok(ReceivePackReportStatus { unpack, commands })
1048}
1049
1050pub fn encode_receive_pack_report_status(
1051 report: &ReceivePackReportStatus,
1052) -> Result<Vec<PktLineFrame>> {
1053 let mut frames = Vec::new();
1054 frames.push(PktLineFrame::data(line_from_str(
1055 &format_receive_pack_unpack_status(&report.unpack)?,
1056 ))?);
1057 for command in &report.commands {
1058 frames.push(PktLineFrame::data(line_from_str(
1059 &format_receive_pack_command_status(command)?,
1060 ))?);
1061 }
1062 frames.push(PktLineFrame::Flush);
1063 Ok(frames)
1064}
1065
1066pub fn read_receive_pack_report_status(reader: &mut impl Read) -> Result<ReceivePackReportStatus> {
1067 let frames = read_pkt_line_frames_until_flush(reader)?;
1068 parse_receive_pack_report_status(&frames)
1069}
1070
1071pub fn write_receive_pack_report_status(
1072 writer: &mut impl Write,
1073 report: &ReceivePackReportStatus,
1074) -> Result<()> {
1075 write_pkt_line_payload(
1076 writer,
1077 &line_from_str(&format_receive_pack_unpack_status(&report.unpack)?),
1078 )?;
1079 for command in &report.commands {
1080 write_pkt_line_payload(
1081 writer,
1082 &line_from_str(&format_receive_pack_command_status(command)?),
1083 )?;
1084 }
1085 writer.write_all(b"0000")?;
1086 Ok(())
1087}
1088
1089pub fn parse_receive_pack_report_status_v2(
1090 format: ObjectFormat,
1091 frames: &[PktLineFrame],
1092) -> Result<ReceivePackReportStatusV2> {
1093 let Some((first, rest)) = frames.split_first() else {
1094 return Err(GitError::InvalidFormat(
1095 "receive-pack report-status-v2 is empty".into(),
1096 ));
1097 };
1098 let PktLineFrame::Data(payload) = first else {
1099 return Err(GitError::InvalidFormat(
1100 "receive-pack report-status-v2 must start with unpack status".into(),
1101 ));
1102 };
1103 let unpack = parse_receive_pack_unpack_status(payload)?;
1104
1105 let mut commands = Vec::new();
1106 let mut saw_flush = false;
1107 for (idx, frame) in rest.iter().enumerate() {
1108 match frame {
1109 PktLineFrame::Data(payload) if !saw_flush => {
1110 let text =
1111 parse_protocol_v2_line_text("receive-pack report-status-v2 line", payload)?;
1112 if text.starts_with("option ") {
1113 let Some(ReceivePackCommandStatusV2::Ok { options, .. }) = commands.last_mut()
1114 else {
1115 return Err(GitError::InvalidFormat(
1116 "receive-pack report-status-v2 option without ok status".into(),
1117 ));
1118 };
1119 parse_receive_pack_report_status_v2_option(format, text, options)?;
1120 } else {
1121 commands.push(parse_receive_pack_command_status_v2(text)?);
1122 }
1123 }
1124 PktLineFrame::Data(_) => {
1125 return Err(GitError::InvalidFormat(
1126 "receive-pack report-status-v2 has data after flush".into(),
1127 ));
1128 }
1129 PktLineFrame::Flush => {
1130 saw_flush = true;
1131 if idx + 1 != rest.len() {
1132 return Err(GitError::InvalidFormat(
1133 "receive-pack report-status-v2 has frames after flush".into(),
1134 ));
1135 }
1136 }
1137 PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
1138 return Err(GitError::InvalidFormat(
1139 "receive-pack report-status-v2 contains a non-flush control packet".into(),
1140 ));
1141 }
1142 }
1143 }
1144 if !saw_flush {
1145 return Err(GitError::InvalidFormat(
1146 "receive-pack report-status-v2 missing flush".into(),
1147 ));
1148 }
1149 Ok(ReceivePackReportStatusV2 { unpack, commands })
1150}
1151
1152pub fn encode_receive_pack_report_status_v2(
1153 report: &ReceivePackReportStatusV2,
1154) -> Result<Vec<PktLineFrame>> {
1155 let mut frames = Vec::new();
1156 frames.push(PktLineFrame::data(line_from_str(
1157 &format_receive_pack_unpack_status(&report.unpack)?,
1158 ))?);
1159 for command in &report.commands {
1160 frames.push(PktLineFrame::data(line_from_str(
1161 &format_receive_pack_command_status_v2(command)?,
1162 ))?);
1163 if let ReceivePackCommandStatusV2::Ok { options, .. } = command {
1164 for option in format_receive_pack_report_status_v2_options(options)? {
1165 frames.push(PktLineFrame::data(line_from_str(&option))?);
1166 }
1167 }
1168 }
1169 frames.push(PktLineFrame::Flush);
1170 Ok(frames)
1171}
1172
1173pub fn read_receive_pack_report_status_v2(
1174 format: ObjectFormat,
1175 reader: &mut impl Read,
1176) -> Result<ReceivePackReportStatusV2> {
1177 let frames = read_pkt_line_frames_until_flush(reader)?;
1178 parse_receive_pack_report_status_v2(format, &frames)
1179}
1180
1181pub fn write_receive_pack_report_status_v2(
1182 writer: &mut impl Write,
1183 report: &ReceivePackReportStatusV2,
1184) -> Result<()> {
1185 write_pkt_line_payload(
1186 writer,
1187 &line_from_str(&format_receive_pack_unpack_status(&report.unpack)?),
1188 )?;
1189 for command in &report.commands {
1190 write_pkt_line_payload(
1191 writer,
1192 &line_from_str(&format_receive_pack_command_status_v2(command)?),
1193 )?;
1194 if let ReceivePackCommandStatusV2::Ok { options, .. } = command {
1195 for option in format_receive_pack_report_status_v2_options(options)? {
1196 write_pkt_line_payload(writer, &line_from_str(&option))?;
1197 }
1198 }
1199 }
1200 writer.write_all(b"0000")?;
1201 Ok(())
1202}
1203
1204pub fn parse_receive_pack_push_options(frames: &[PktLineFrame]) -> Result<Vec<String>> {
1205 let mut options = Vec::new();
1206 let mut saw_flush = false;
1207 for (idx, frame) in frames.iter().enumerate() {
1208 match frame {
1209 PktLineFrame::Data(payload) if !saw_flush => {
1210 let option = trim_trailing_lf(payload);
1211 validate_receive_pack_push_option(option)?;
1212 options.push(
1213 std::str::from_utf8(option)
1214 .map_err(|err| GitError::InvalidFormat(err.to_string()))?
1215 .to_string(),
1216 );
1217 }
1218 PktLineFrame::Data(_) => {
1219 return Err(GitError::InvalidFormat(
1220 "receive-pack push-options has data after flush".into(),
1221 ));
1222 }
1223 PktLineFrame::Flush => {
1224 saw_flush = true;
1225 if idx + 1 != frames.len() {
1226 return Err(GitError::InvalidFormat(
1227 "receive-pack push-options has frames after flush".into(),
1228 ));
1229 }
1230 }
1231 PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
1232 return Err(GitError::InvalidFormat(
1233 "receive-pack push-options contains a non-flush control packet".into(),
1234 ));
1235 }
1236 }
1237 }
1238 if !saw_flush {
1239 return Err(GitError::InvalidFormat(
1240 "receive-pack push-options missing flush".into(),
1241 ));
1242 }
1243 Ok(options)
1244}
1245
1246pub fn encode_receive_pack_push_options(options: &[String]) -> Result<Vec<PktLineFrame>> {
1247 let mut frames = Vec::new();
1248 for option in options {
1249 validate_receive_pack_push_option(option.as_bytes())?;
1250 let mut payload = option.as_bytes().to_vec();
1251 payload.push(b'\n');
1252 frames.push(PktLineFrame::data(payload)?);
1253 }
1254 frames.push(PktLineFrame::Flush);
1255 Ok(frames)
1256}
1257
1258pub fn read_receive_pack_push_options(reader: &mut impl Read) -> Result<Vec<String>> {
1259 let frames = read_pkt_line_frames_until_flush(reader)?;
1260 parse_receive_pack_push_options(&frames)
1261}
1262
1263pub fn write_receive_pack_push_options(writer: &mut impl Write, options: &[String]) -> Result<()> {
1264 for option in options {
1265 validate_receive_pack_push_option(option.as_bytes())?;
1266 let mut payload = option.as_bytes().to_vec();
1267 payload.push(b'\n');
1268 write_pkt_line_payload(writer, &payload)?;
1269 }
1270 writer.write_all(b"0000")?;
1271 Ok(())
1272}
1273pub(crate) fn zero_object_id(format: ObjectFormat) -> Result<ObjectId> {
1274 Ok(ObjectId::null(format))
1275}
1276
1277fn local_ref<'a>(refs: &'a [PushSourceRef], name: &str) -> Option<&'a PushSourceRef> {
1278 refs.iter().find(|reference| reference.name == name)
1279}
1280
1281fn remote_ref<'a>(refs: &'a [RefAdvertisement], name: &str) -> Option<&'a RefAdvertisement> {
1282 refs.iter().find(|reference| reference.name == name)
1283}
1284
1285fn validate_push_source_ref(format: ObjectFormat, reference: &PushSourceRef) -> Result<()> {
1286 if reference.oid.format() != format {
1287 return Err(GitError::InvalidObjectId(
1288 "push source ref object format does not match repository".into(),
1289 ));
1290 }
1291 validate_refspec_endpoint("push source ref name", &reference.name)
1292}
1293
1294fn require_receive_pack_feature(advertised: bool, name: &str) -> Result<()> {
1295 if advertised {
1296 Ok(())
1297 } else {
1298 Err(GitError::InvalidFormat(format!(
1299 "receive-pack feature {name} was not advertised"
1300 )))
1301 }
1302}
1303
1304fn parse_receive_pack_command(format: ObjectFormat, payload: &[u8]) -> Result<ReceivePackCommand> {
1305 let text =
1306 std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
1307 let mut fields = text.split(' ');
1308 let old_id = fields
1309 .next()
1310 .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing old id".into()))?;
1311 let new_id = fields
1312 .next()
1313 .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing new id".into()))?;
1314 let name = fields
1315 .next()
1316 .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing ref name".into()))?;
1317 if fields.next().is_some() {
1318 return Err(GitError::InvalidFormat(
1319 "receive-pack command has too many fields".into(),
1320 ));
1321 }
1322 validate_protocol_v2_token("receive-pack old id", old_id)?;
1323 validate_protocol_v2_token("receive-pack new id", new_id)?;
1324 validate_protocol_v2_token("receive-pack ref name", name)?;
1325 Ok(ReceivePackCommand {
1326 old_id: ObjectId::from_hex(format, old_id)?,
1327 new_id: ObjectId::from_hex(format, new_id)?,
1328 name: name.to_string(),
1329 })
1330}
1331
1332fn format_receive_pack_command(command: &ReceivePackCommand) -> Result<Vec<u8>> {
1333 if command.old_id.format() != command.new_id.format() {
1334 return Err(GitError::InvalidObjectId(
1335 "receive-pack command object formats do not match".into(),
1336 ));
1337 }
1338 validate_protocol_v2_token("receive-pack ref name", &command.name)?;
1339 Ok(format!("{} {} {}", command.old_id, command.new_id, command.name).into_bytes())
1340}
1341
1342pub(crate) fn reject_capability_value(label: &str, capability: &Capability) -> Result<()> {
1343 if capability.value.is_some() {
1344 return Err(GitError::InvalidFormat(format!(
1345 "{label} must not have value"
1346 )));
1347 }
1348 Ok(())
1349}
1350fn is_known_receive_pack_capability(name: &str) -> bool {
1351 matches!(
1352 name,
1353 "report-status"
1354 | "report-status-v2"
1355 | "delete-refs"
1356 | "ofs-delta"
1357 | "atomic"
1358 | "push-options"
1359 | "side-band-64k"
1360 | "quiet"
1361 | "no-thin"
1362 | "agent"
1363 | "object-format"
1364 )
1365}
1366
1367fn is_receive_pack_delete_command(command: &ReceivePackCommand) -> bool {
1368 command.new_id.is_null()
1369}
1370
1371fn parse_receive_pack_unpack_status(payload: &[u8]) -> Result<ReceivePackUnpackStatus> {
1372 let text = parse_protocol_v2_line_text("receive-pack unpack status", payload)?;
1373 if text == "unpack ok" {
1374 return Ok(ReceivePackUnpackStatus::Ok);
1375 }
1376 let Some(message) = text.strip_prefix("unpack ") else {
1377 return Err(GitError::InvalidFormat(format!(
1378 "unsupported receive-pack unpack status {text}"
1379 )));
1380 };
1381 validate_receive_pack_status_message("receive-pack unpack error", message)?;
1382 Ok(ReceivePackUnpackStatus::Error(message.to_string()))
1383}
1384
1385fn format_receive_pack_unpack_status(status: &ReceivePackUnpackStatus) -> Result<String> {
1386 match status {
1387 ReceivePackUnpackStatus::Ok => Ok("unpack ok".into()),
1388 ReceivePackUnpackStatus::Error(message) => {
1389 validate_receive_pack_status_message("receive-pack unpack error", message)?;
1390 Ok(format!("unpack {message}"))
1391 }
1392 }
1393}
1394
1395fn parse_receive_pack_command_status(payload: &[u8]) -> Result<ReceivePackCommandStatus> {
1396 let text = parse_protocol_v2_line_text("receive-pack command status", payload)?;
1397 if let Some(name) = text.strip_prefix("ok ") {
1398 validate_protocol_v2_token("receive-pack status ref name", name)?;
1399 return Ok(ReceivePackCommandStatus::Ok {
1400 name: name.to_string(),
1401 });
1402 }
1403 let Some(rest) = text.strip_prefix("ng ") else {
1404 return Err(GitError::InvalidFormat(format!(
1405 "unsupported receive-pack command status {text}"
1406 )));
1407 };
1408 let (name, message) = rest
1409 .split_once(' ')
1410 .ok_or_else(|| GitError::InvalidFormat("receive-pack ng status missing message".into()))?;
1411 validate_protocol_v2_token("receive-pack status ref name", name)?;
1412 validate_receive_pack_status_message("receive-pack ng status message", message)?;
1413 Ok(ReceivePackCommandStatus::Ng {
1414 name: name.to_string(),
1415 message: message.to_string(),
1416 })
1417}
1418
1419fn format_receive_pack_command_status(status: &ReceivePackCommandStatus) -> Result<String> {
1420 match status {
1421 ReceivePackCommandStatus::Ok { name } => {
1422 validate_protocol_v2_token("receive-pack status ref name", name)?;
1423 Ok(format!("ok {name}"))
1424 }
1425 ReceivePackCommandStatus::Ng { name, message } => {
1426 validate_protocol_v2_token("receive-pack status ref name", name)?;
1427 validate_receive_pack_status_message("receive-pack ng status message", message)?;
1428 Ok(format!("ng {name} {message}"))
1429 }
1430 }
1431}
1432
1433fn parse_receive_pack_command_status_v2(text: &str) -> Result<ReceivePackCommandStatusV2> {
1434 if let Some(name) = text.strip_prefix("ok ") {
1435 validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
1436 return Ok(ReceivePackCommandStatusV2::Ok {
1437 name: name.to_string(),
1438 options: ReceivePackCommandStatusV2Options::default(),
1439 });
1440 }
1441 let Some(rest) = text.strip_prefix("ng ") else {
1442 return Err(GitError::InvalidFormat(format!(
1443 "unsupported receive-pack report-status-v2 line {text}"
1444 )));
1445 };
1446 let (name, message) = rest.split_once(' ').ok_or_else(|| {
1447 GitError::InvalidFormat("receive-pack status-v2 ng status missing message".into())
1448 })?;
1449 validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
1450 validate_receive_pack_status_message("receive-pack status-v2 ng message", message)?;
1451 Ok(ReceivePackCommandStatusV2::Ng {
1452 name: name.to_string(),
1453 message: message.to_string(),
1454 })
1455}
1456
1457fn format_receive_pack_command_status_v2(status: &ReceivePackCommandStatusV2) -> Result<String> {
1458 match status {
1459 ReceivePackCommandStatusV2::Ok { name, .. } => {
1460 validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
1461 Ok(format!("ok {name}"))
1462 }
1463 ReceivePackCommandStatusV2::Ng { name, message } => {
1464 validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
1465 validate_receive_pack_status_message("receive-pack status-v2 ng message", message)?;
1466 Ok(format!("ng {name} {message}"))
1467 }
1468 }
1469}
1470
1471fn parse_receive_pack_report_status_v2_option(
1472 format: ObjectFormat,
1473 text: &str,
1474 options: &mut ReceivePackCommandStatusV2Options,
1475) -> Result<()> {
1476 if let Some(refname) = text.strip_prefix("option refname ") {
1477 if options.refname.is_some() {
1478 return Err(GitError::InvalidFormat(
1479 "receive-pack report-status-v2 has duplicate refname option".into(),
1480 ));
1481 }
1482 validate_protocol_v2_token("receive-pack status-v2 option refname", refname)?;
1483 options.refname = Some(refname.to_string());
1484 } else if let Some(old_oid) = text.strip_prefix("option old-oid ") {
1485 if options.old_oid.is_some() {
1486 return Err(GitError::InvalidFormat(
1487 "receive-pack report-status-v2 has duplicate old-oid option".into(),
1488 ));
1489 }
1490 validate_protocol_v2_token("receive-pack status-v2 option old-oid", old_oid)?;
1491 options.old_oid = Some(ObjectId::from_hex(format, old_oid)?);
1492 } else if let Some(new_oid) = text.strip_prefix("option new-oid ") {
1493 if options.new_oid.is_some() {
1494 return Err(GitError::InvalidFormat(
1495 "receive-pack report-status-v2 has duplicate new-oid option".into(),
1496 ));
1497 }
1498 validate_protocol_v2_token("receive-pack status-v2 option new-oid", new_oid)?;
1499 options.new_oid = Some(ObjectId::from_hex(format, new_oid)?);
1500 } else if text == "option forced-update" {
1501 if options.forced_update {
1502 return Err(GitError::InvalidFormat(
1503 "receive-pack report-status-v2 has duplicate forced-update option".into(),
1504 ));
1505 }
1506 options.forced_update = true;
1507 } else {
1508 return Err(GitError::InvalidFormat(format!(
1509 "unsupported receive-pack report-status-v2 option {text}"
1510 )));
1511 }
1512 Ok(())
1513}
1514
1515fn format_receive_pack_report_status_v2_options(
1516 options: &ReceivePackCommandStatusV2Options,
1517) -> Result<Vec<String>> {
1518 let mut out = Vec::new();
1519 if let Some(refname) = &options.refname {
1520 validate_protocol_v2_token("receive-pack status-v2 option refname", refname)?;
1521 out.push(format!("option refname {refname}"));
1522 }
1523 if let Some(old_oid) = &options.old_oid {
1524 out.push(format!("option old-oid {old_oid}"));
1525 }
1526 if let Some(new_oid) = &options.new_oid {
1527 out.push(format!("option new-oid {new_oid}"));
1528 }
1529 if options.forced_update {
1530 out.push("option forced-update".into());
1531 }
1532 Ok(out)
1533}
1534
1535fn validate_receive_pack_status_message(label: &str, message: &str) -> Result<()> {
1536 if message.is_empty() {
1537 return Err(GitError::InvalidFormat(format!("{label} is empty")));
1538 }
1539 if message
1540 .bytes()
1541 .any(|byte| matches!(byte, b'\n' | b'\r' | 0))
1542 {
1543 return Err(GitError::InvalidFormat(format!(
1544 "{label} contains a delimiter byte"
1545 )));
1546 }
1547 Ok(())
1548}
1549
1550fn validate_receive_pack_push_option(option: &[u8]) -> Result<()> {
1551 if option.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
1552 return Err(GitError::InvalidFormat(
1553 "receive-pack push-option contains a delimiter byte".into(),
1554 ));
1555 }
1556 Ok(())
1557}