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