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