Skip to main content

sley_protocol/
receive_pack.rs

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                // A bare ":" (matching) refspec pushes only refs the remote
131                // already has, by their fully-qualified `refs/...` name. git's
132                // matching source set is the local ref advertisement, which never
133                // includes `HEAD` or short-name aliases — push those would try to
134                // update the remote's `HEAD`, so skip anything not under `refs/`.
135                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}