Skip to main content

sley_protocol/
upload_pack.rs

1use sley_core::{Capability, GitError, ObjectFormat, ObjectId, Result};
2use std::io::{Read, Write};
3
4use crate::pktline::{
5    PKT_LINE_MAX_LEN, PktLineFrame, line_from_str, parse_oid_argument, parse_pkt_len,
6    parse_protocol_v2_line_text, read_pkt_line_frame, read_pkt_line_frames_until_flush,
7    trace_packet_read_payload, trim_trailing_lf, validate_capability_field,
8    validate_protocol_v2_token, write_pkt_line_payload,
9};
10use crate::receive_pack::reject_capability_value;
11use crate::sideband::{
12    SideBandDemux, SideBandPacket, demux_sideband_packets, encode_sideband_packet,
13    parse_sideband_packet, write_sideband_packet,
14};
15use crate::v0::{encode_capabilities, parse_capabilities};
16use crate::v2::{
17    ProtocolV2FetchShallowInfo, parse_fetch_shallow_info, parse_u32_argument, parse_u64_argument,
18};
19
20#[derive(Debug, Clone, PartialEq, Eq, Default)]
21pub struct UploadPackRequest {
22    pub wants: Vec<ObjectId>,
23    pub capabilities: Vec<Capability>,
24    pub shallow: Vec<ObjectId>,
25    pub deepen: Option<u32>,
26    pub deepen_since: Option<u64>,
27    pub deepen_not: Vec<String>,
28    pub filter: Option<String>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Default)]
32pub struct UploadPackFeatures {
33    pub multi_ack: bool,
34    pub multi_ack_detailed: bool,
35    pub no_done: bool,
36    pub thin_pack: bool,
37    pub side_band: bool,
38    pub side_band_64k: bool,
39    pub ofs_delta: bool,
40    pub shallow: bool,
41    pub deepen_since: bool,
42    pub deepen_not: bool,
43    pub include_tag: bool,
44    pub no_progress: bool,
45    pub allow_tip_sha1_in_want: bool,
46    pub allow_reachable_sha1_in_want: bool,
47    pub filter: bool,
48    pub agent: Option<String>,
49    pub object_format: Option<ObjectFormat>,
50    pub symrefs: Vec<String>,
51    pub unknown: Vec<Capability>,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Default)]
55pub struct UploadPackNegotiationRequest {
56    pub haves: Vec<ObjectId>,
57    pub done: bool,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum UploadPackAckStatus {
62    Continue,
63    Common,
64    Ready,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum UploadPackAcknowledgment {
69    Nak,
70    Ack {
71        oid: ObjectId,
72        status: Option<UploadPackAckStatus>,
73    },
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Default)]
77pub struct UploadPackPackfileResponse {
78    pub acknowledgments: Vec<UploadPackAcknowledgment>,
79    pub sideband: Vec<SideBandPacket>,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Default)]
83pub struct UploadPackRawPackfileResponse {
84    pub acknowledgments: Vec<UploadPackAcknowledgment>,
85    pub packfile: Vec<u8>,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Default)]
89pub struct UploadPackRawPackfileResponseHeader {
90    pub acknowledgments: Vec<UploadPackAcknowledgment>,
91    pub pack_prefix: Vec<u8>,
92}
93pub fn parse_upload_pack_request(
94    format: ObjectFormat,
95    frames: &[PktLineFrame],
96) -> Result<Option<UploadPackRequest>> {
97    if matches!(frames, [PktLineFrame::Flush]) {
98        return Ok(None);
99    }
100
101    let mut request = UploadPackRequest::default();
102    let mut in_options = false;
103    let mut saw_flush = false;
104    for (idx, frame) in frames.iter().enumerate() {
105        match frame {
106            PktLineFrame::Data(payload) if !saw_flush => {
107                let text = parse_protocol_v2_line_text("upload-pack request line", payload)?;
108                if let Some(value) = text.strip_prefix("want ") {
109                    if in_options {
110                        return Err(GitError::InvalidFormat(
111                            "upload-pack request has want after options".into(),
112                        ));
113                    }
114                    let (oid, capabilities) = if request.wants.is_empty() {
115                        value
116                            .split_once(' ')
117                            .map_or((value, None), |(oid, caps)| (oid, Some(caps.as_bytes())))
118                    } else {
119                        if value.contains(' ') {
120                            return Err(GitError::InvalidFormat(
121                                "additional upload-pack want has capabilities".into(),
122                            ));
123                        }
124                        (value, None)
125                    };
126                    validate_protocol_v2_token("upload-pack want", oid)?;
127                    request.wants.push(ObjectId::from_hex(format, oid)?);
128                    if let Some(capabilities) = capabilities {
129                        request.capabilities = parse_capabilities(capabilities)?;
130                    }
131                    continue;
132                }
133
134                if request.wants.is_empty() {
135                    return Err(GitError::InvalidFormat(
136                        "upload-pack request must start with want".into(),
137                    ));
138                }
139                in_options = true;
140                if text.starts_with("shallow ") {
141                    request.shallow.push(parse_oid_argument(
142                        format,
143                        "upload-pack shallow",
144                        text,
145                        "shallow ",
146                    )?);
147                } else if text.starts_with("deepen ") {
148                    if request.deepen.is_some() {
149                        return Err(GitError::InvalidFormat(
150                            "upload-pack request has duplicate deepen".into(),
151                        ));
152                    }
153                    request.deepen =
154                        Some(parse_u32_argument("upload-pack deepen", text, "deepen ")?);
155                } else if text.starts_with("deepen-since ") {
156                    if request.deepen_since.is_some() {
157                        return Err(GitError::InvalidFormat(
158                            "upload-pack request has duplicate deepen-since".into(),
159                        ));
160                    }
161                    request.deepen_since = Some(parse_u64_argument(
162                        "upload-pack deepen-since",
163                        text,
164                        "deepen-since ",
165                    )?);
166                } else if let Some(name) = text.strip_prefix("deepen-not ") {
167                    validate_protocol_v2_token("upload-pack deepen-not", name)?;
168                    request.deepen_not.push(name.to_string());
169                } else if let Some(filter) = text.strip_prefix("filter ") {
170                    if request.filter.is_some() {
171                        return Err(GitError::InvalidFormat(
172                            "upload-pack request has duplicate filter".into(),
173                        ));
174                    }
175                    validate_protocol_v2_token("upload-pack filter", filter)?;
176                    request.filter = Some(filter.to_string());
177                } else {
178                    return Err(GitError::InvalidFormat(format!(
179                        "unsupported upload-pack request line {text}"
180                    )));
181                }
182            }
183            PktLineFrame::Data(_) => {
184                return Err(GitError::InvalidFormat(
185                    "upload-pack request has data after flush".into(),
186                ));
187            }
188            PktLineFrame::Flush => {
189                saw_flush = true;
190                if idx + 1 != frames.len() {
191                    return Err(GitError::InvalidFormat(
192                        "upload-pack request has frames after flush".into(),
193                    ));
194                }
195            }
196            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
197                return Err(GitError::InvalidFormat(
198                    "upload-pack request contains a non-flush control packet".into(),
199                ));
200            }
201        }
202    }
203    if !saw_flush {
204        return Err(GitError::InvalidFormat(
205            "upload-pack request missing flush".into(),
206        ));
207    }
208    if request.wants.is_empty() {
209        return Err(GitError::InvalidFormat(
210            "upload-pack request missing want".into(),
211        ));
212    }
213    Ok(Some(request))
214}
215
216pub fn encode_upload_pack_request(
217    request: Option<&UploadPackRequest>,
218) -> Result<Vec<PktLineFrame>> {
219    let Some(request) = request else {
220        return Ok(vec![PktLineFrame::Flush]);
221    };
222    if request.wants.is_empty() {
223        return Err(GitError::InvalidFormat(
224            "upload-pack request missing want".into(),
225        ));
226    }
227
228    let mut frames = Vec::new();
229    for (idx, oid) in request.wants.iter().enumerate() {
230        let mut line = format!("want {oid}");
231        if idx == 0 && !request.capabilities.is_empty() {
232            line.push(' ');
233            line.push_str(
234                &String::from_utf8(encode_capabilities(&request.capabilities)?)
235                    .map_err(|err| GitError::InvalidFormat(err.to_string()))?,
236            );
237        }
238        frames.push(PktLineFrame::data(line_from_str(&line))?);
239    }
240    for oid in &request.shallow {
241        frames.push(PktLineFrame::data(line_from_str(&format!(
242            "shallow {oid}"
243        )))?);
244    }
245    if let Some(deepen) = request.deepen {
246        if deepen == 0 {
247            return Err(GitError::InvalidFormat(
248                "upload-pack deepen must be positive".into(),
249            ));
250        }
251        frames.push(PktLineFrame::data(line_from_str(&format!(
252            "deepen {deepen}"
253        )))?);
254    }
255    if let Some(deepen_since) = request.deepen_since {
256        frames.push(PktLineFrame::data(line_from_str(&format!(
257            "deepen-since {deepen_since}"
258        )))?);
259    }
260    for name in &request.deepen_not {
261        validate_protocol_v2_token("upload-pack deepen-not", name)?;
262        frames.push(PktLineFrame::data(line_from_str(&format!(
263            "deepen-not {name}"
264        )))?);
265    }
266    if let Some(filter) = &request.filter {
267        validate_protocol_v2_token("upload-pack filter", filter)?;
268        frames.push(PktLineFrame::data(line_from_str(&format!(
269            "filter {filter}"
270        )))?);
271    }
272    frames.push(PktLineFrame::Flush);
273    Ok(frames)
274}
275
276pub fn read_upload_pack_request(
277    format: ObjectFormat,
278    reader: &mut impl Read,
279) -> Result<Option<UploadPackRequest>> {
280    let frames = read_pkt_line_frames_until_flush(reader)?;
281    parse_upload_pack_request(format, &frames)
282}
283
284pub fn write_upload_pack_request(
285    writer: &mut impl Write,
286    request: Option<&UploadPackRequest>,
287) -> Result<()> {
288    let Some(request) = request else {
289        writer.write_all(b"0000")?;
290        return Ok(());
291    };
292    if request.wants.is_empty() {
293        return Err(GitError::InvalidFormat(
294            "upload-pack request missing want".into(),
295        ));
296    }
297
298    for (idx, oid) in request.wants.iter().enumerate() {
299        let mut line = format!("want {oid}");
300        if idx == 0 && !request.capabilities.is_empty() {
301            line.push(' ');
302            line.push_str(
303                &String::from_utf8(encode_capabilities(&request.capabilities)?)
304                    .map_err(|err| GitError::InvalidFormat(err.to_string()))?,
305            );
306        }
307        write_pkt_line_payload(writer, &line_from_str(&line))?;
308    }
309    for oid in &request.shallow {
310        write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
311    }
312    if let Some(deepen) = request.deepen {
313        if deepen == 0 {
314            return Err(GitError::InvalidFormat(
315                "upload-pack deepen must be positive".into(),
316            ));
317        }
318        write_pkt_line_payload(writer, &line_from_str(&format!("deepen {deepen}")))?;
319    }
320    if let Some(deepen_since) = request.deepen_since {
321        write_pkt_line_payload(
322            writer,
323            &line_from_str(&format!("deepen-since {deepen_since}")),
324        )?;
325    }
326    for name in &request.deepen_not {
327        validate_protocol_v2_token("upload-pack deepen-not", name)?;
328        write_pkt_line_payload(writer, &line_from_str(&format!("deepen-not {name}")))?;
329    }
330    if let Some(filter) = &request.filter {
331        validate_protocol_v2_token("upload-pack filter", filter)?;
332        write_pkt_line_payload(writer, &line_from_str(&format!("filter {filter}")))?;
333    }
334    writer.write_all(b"0000")?;
335    Ok(())
336}
337
338pub fn parse_upload_pack_features(capabilities: &[Capability]) -> Result<UploadPackFeatures> {
339    let mut features = UploadPackFeatures::default();
340    for capability in capabilities {
341        match capability.name.as_str() {
342            "multi_ack" => set_upload_pack_flag(&mut features.multi_ack, capability)?,
343            "multi_ack_detailed" => {
344                set_upload_pack_flag(&mut features.multi_ack_detailed, capability)?
345            }
346            "no-done" => set_upload_pack_flag(&mut features.no_done, capability)?,
347            "thin-pack" => set_upload_pack_flag(&mut features.thin_pack, capability)?,
348            "side-band" => set_upload_pack_flag(&mut features.side_band, capability)?,
349            "side-band-64k" => set_upload_pack_flag(&mut features.side_band_64k, capability)?,
350            "ofs-delta" => set_upload_pack_flag(&mut features.ofs_delta, capability)?,
351            "shallow" => set_upload_pack_flag(&mut features.shallow, capability)?,
352            "deepen-since" => set_upload_pack_flag(&mut features.deepen_since, capability)?,
353            "deepen-not" => set_upload_pack_flag(&mut features.deepen_not, capability)?,
354            "include-tag" => set_upload_pack_flag(&mut features.include_tag, capability)?,
355            "no-progress" => set_upload_pack_flag(&mut features.no_progress, capability)?,
356            "allow-tip-sha1-in-want" => {
357                set_upload_pack_flag(&mut features.allow_tip_sha1_in_want, capability)?
358            }
359            "allow-reachable-sha1-in-want" => {
360                set_upload_pack_flag(&mut features.allow_reachable_sha1_in_want, capability)?
361            }
362            "filter" => set_upload_pack_flag(&mut features.filter, capability)?,
363            "agent" => {
364                let Some(agent) = &capability.value else {
365                    return Err(GitError::InvalidFormat(
366                        "upload-pack agent capability is missing value".into(),
367                    ));
368                };
369                if features.agent.is_some() {
370                    return Err(GitError::InvalidFormat(
371                        "upload-pack has duplicate agent capability".into(),
372                    ));
373                }
374                validate_capability_field("upload-pack agent", agent)?;
375                features.agent = Some(agent.clone());
376            }
377            "object-format" => {
378                let Some(format) = &capability.value else {
379                    return Err(GitError::InvalidFormat(
380                        "upload-pack object-format capability is missing value".into(),
381                    ));
382                };
383                if features.object_format.is_some() {
384                    return Err(GitError::InvalidFormat(
385                        "upload-pack has duplicate object-format capability".into(),
386                    ));
387                }
388                validate_capability_field("upload-pack object-format", format)?;
389                features.object_format = Some(format.parse()?);
390            }
391            "symref" => {
392                let Some(symref) = &capability.value else {
393                    continue;
394                };
395                validate_capability_field("upload-pack symref", symref)?;
396                features.symrefs.push(symref.clone());
397            }
398            _ => {
399                encode_capabilities(std::slice::from_ref(capability))?;
400                if features
401                    .unknown
402                    .iter()
403                    .any(|known| known.name == capability.name)
404                {
405                    return Err(GitError::InvalidFormat(format!(
406                        "upload-pack has duplicate {} capability",
407                        capability.name
408                    )));
409                }
410                features.unknown.push(capability.clone());
411            }
412        }
413    }
414    Ok(features)
415}
416
417pub fn encode_upload_pack_features(features: &UploadPackFeatures) -> Result<Vec<Capability>> {
418    let mut capabilities = Vec::new();
419    push_upload_pack_flag(&mut capabilities, "multi_ack", features.multi_ack);
420    push_upload_pack_flag(
421        &mut capabilities,
422        "multi_ack_detailed",
423        features.multi_ack_detailed,
424    );
425    push_upload_pack_flag(&mut capabilities, "no-done", features.no_done);
426    push_upload_pack_flag(&mut capabilities, "thin-pack", features.thin_pack);
427    push_upload_pack_flag(&mut capabilities, "side-band", features.side_band);
428    push_upload_pack_flag(&mut capabilities, "side-band-64k", features.side_band_64k);
429    push_upload_pack_flag(&mut capabilities, "ofs-delta", features.ofs_delta);
430    push_upload_pack_flag(&mut capabilities, "shallow", features.shallow);
431    push_upload_pack_flag(&mut capabilities, "deepen-since", features.deepen_since);
432    push_upload_pack_flag(&mut capabilities, "deepen-not", features.deepen_not);
433    push_upload_pack_flag(&mut capabilities, "include-tag", features.include_tag);
434    push_upload_pack_flag(&mut capabilities, "no-progress", features.no_progress);
435    push_upload_pack_flag(
436        &mut capabilities,
437        "allow-tip-sha1-in-want",
438        features.allow_tip_sha1_in_want,
439    );
440    push_upload_pack_flag(
441        &mut capabilities,
442        "allow-reachable-sha1-in-want",
443        features.allow_reachable_sha1_in_want,
444    );
445    push_upload_pack_flag(&mut capabilities, "filter", features.filter);
446    if let Some(agent) = &features.agent {
447        validate_capability_field("upload-pack agent", agent)?;
448        capabilities.push(Capability {
449            name: "agent".into(),
450            value: Some(agent.clone()),
451        });
452    }
453    if let Some(format) = features.object_format {
454        capabilities.push(Capability {
455            name: "object-format".into(),
456            value: Some(format.name().into()),
457        });
458    }
459    for symref in &features.symrefs {
460        validate_capability_field("upload-pack symref", symref)?;
461        capabilities.push(Capability {
462            name: "symref".into(),
463            value: Some(symref.clone()),
464        });
465    }
466    for capability in &features.unknown {
467        if is_known_upload_pack_capability(&capability.name) {
468            return Err(GitError::InvalidFormat(format!(
469                "upload-pack unknown capability duplicates known capability {}",
470                capability.name
471            )));
472        }
473        encode_capabilities(std::slice::from_ref(capability))?;
474        capabilities.push(capability.clone());
475    }
476    Ok(capabilities)
477}
478
479pub fn validate_upload_pack_request_features(
480    features: &UploadPackFeatures,
481    request: &UploadPackRequest,
482) -> Result<()> {
483    for capability in &request.capabilities {
484        if is_upload_pack_flag_capability(&capability.name) {
485            reject_capability_value("upload-pack request capability", capability)?;
486        }
487        match capability.name.as_str() {
488            "multi_ack" if !features.multi_ack => {
489                return Err(GitError::InvalidFormat(
490                    "upload-pack request uses multi_ack without advertised capability".into(),
491                ));
492            }
493            "multi_ack_detailed" if !features.multi_ack_detailed => {
494                return Err(GitError::InvalidFormat(
495                    "upload-pack request uses multi_ack_detailed without advertised capability"
496                        .into(),
497                ));
498            }
499            "no-done" if !features.no_done => {
500                return Err(GitError::InvalidFormat(
501                    "upload-pack request uses no-done without advertised capability".into(),
502                ));
503            }
504            "thin-pack" if !features.thin_pack => {
505                return Err(GitError::InvalidFormat(
506                    "upload-pack request uses thin-pack without advertised capability".into(),
507                ));
508            }
509            "side-band" if !features.side_band => {
510                return Err(GitError::InvalidFormat(
511                    "upload-pack request uses side-band without advertised capability".into(),
512                ));
513            }
514            "side-band-64k" if !features.side_band_64k => {
515                return Err(GitError::InvalidFormat(
516                    "upload-pack request uses side-band-64k without advertised capability".into(),
517                ));
518            }
519            "ofs-delta" if !features.ofs_delta => {
520                return Err(GitError::InvalidFormat(
521                    "upload-pack request uses ofs-delta without advertised capability".into(),
522                ));
523            }
524            "include-tag" if !features.include_tag => {
525                return Err(GitError::InvalidFormat(
526                    "upload-pack request uses include-tag without advertised capability".into(),
527                ));
528            }
529            "no-progress" if !features.no_progress => {
530                return Err(GitError::InvalidFormat(
531                    "upload-pack request uses no-progress without advertised capability".into(),
532                ));
533            }
534            "allow-tip-sha1-in-want" if !features.allow_tip_sha1_in_want => {
535                return Err(GitError::InvalidFormat(
536                    "upload-pack request uses allow-tip-sha1-in-want without advertised capability"
537                        .into(),
538                ));
539            }
540            "allow-reachable-sha1-in-want" if !features.allow_reachable_sha1_in_want => {
541                return Err(GitError::InvalidFormat(
542                    "upload-pack request uses allow-reachable-sha1-in-want without advertised capability"
543                        .into(),
544                ));
545            }
546            "filter" if !features.filter => {
547                return Err(GitError::InvalidFormat(
548                    "upload-pack request uses filter capability without advertised capability"
549                        .into(),
550                ));
551            }
552            "agent" => {
553                let Some(agent) = &capability.value else {
554                    return Err(GitError::InvalidFormat(
555                        "upload-pack request agent capability is missing value".into(),
556                    ));
557                };
558                validate_capability_field("upload-pack request agent", agent)?;
559            }
560            "object-format" => {
561                let Some(format) = &capability.value else {
562                    return Err(GitError::InvalidFormat(
563                        "upload-pack request object-format capability is missing value".into(),
564                    ));
565                };
566                let requested_format: ObjectFormat = format.parse()?;
567                if features.object_format != Some(requested_format) {
568                    return Err(GitError::InvalidFormat(
569                        "upload-pack request object-format was not advertised".into(),
570                    ));
571                }
572            }
573            name if is_known_upload_pack_capability(name) => {}
574            _ => {
575                if !features
576                    .unknown
577                    .iter()
578                    .any(|advertised| advertised.name == capability.name)
579                {
580                    return Err(GitError::InvalidFormat(format!(
581                        "upload-pack request uses unadvertised capability {}",
582                        capability.name
583                    )));
584                }
585            }
586        }
587    }
588
589    let sideband = request
590        .capabilities
591        .iter()
592        .any(|capability| capability.name == "side-band");
593    let sideband_64k = request
594        .capabilities
595        .iter()
596        .any(|capability| capability.name == "side-band-64k");
597    if sideband && sideband_64k {
598        return Err(GitError::InvalidFormat(
599            "upload-pack request must not request both side-band and side-band-64k".into(),
600        ));
601    }
602
603    if !features.shallow && (!request.shallow.is_empty() || request.deepen.is_some()) {
604        return Err(GitError::InvalidFormat(
605            "upload-pack request uses shallow/deepen without advertised shallow capability".into(),
606        ));
607    }
608    if !features.deepen_since && request.deepen_since.is_some() {
609        return Err(GitError::InvalidFormat(
610            "upload-pack request uses deepen-since without advertised capability".into(),
611        ));
612    }
613    if !features.deepen_not && !request.deepen_not.is_empty() {
614        return Err(GitError::InvalidFormat(
615            "upload-pack request uses deepen-not without advertised capability".into(),
616        ));
617    }
618    if !features.filter && request.filter.is_some() {
619        return Err(GitError::InvalidFormat(
620            "upload-pack request uses filter without advertised capability".into(),
621        ));
622    }
623    Ok(())
624}
625
626pub fn build_upload_pack_raw_packfile_response<C, B>(
627    features: &UploadPackFeatures,
628    request: UploadPackRequest,
629    haves: impl IntoIterator<Item = ObjectId>,
630    mut contains_object: C,
631    mut build_pack: B,
632) -> Result<UploadPackRawPackfileResponse>
633where
634    C: FnMut(&ObjectId) -> Result<bool>,
635    B: FnMut(Vec<ObjectId>, Vec<ObjectId>) -> Result<Option<Vec<u8>>>,
636{
637    validate_upload_pack_request_features(features, &request)?;
638    for want in &request.wants {
639        if !contains_object(want)? {
640            return Err(GitError::InvalidObject(format!(
641                "upload-pack requested missing object {want}"
642            )));
643        }
644    }
645    let known_haves = haves
646        .into_iter()
647        .filter_map(|oid| match contains_object(&oid) {
648            Ok(true) => Some(Ok(oid)),
649            Ok(false) => None,
650            Err(err) => Some(Err(err)),
651        })
652        .collect::<Result<Vec<_>>>()?;
653    let packfile = build_pack(request.wants, known_haves)?
654        .ok_or_else(|| GitError::InvalidObject("upload-pack request produced empty pack".into()))?;
655    Ok(UploadPackRawPackfileResponse {
656        acknowledgments: vec![UploadPackAcknowledgment::Nak],
657        packfile,
658    })
659}
660
661pub fn parse_upload_pack_shallow_update(
662    format: ObjectFormat,
663    frames: &[PktLineFrame],
664) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
665    let mut entries = Vec::new();
666    let mut saw_flush = false;
667    for (idx, frame) in frames.iter().enumerate() {
668        match frame {
669            PktLineFrame::Data(payload) if !saw_flush => {
670                entries.push(parse_fetch_shallow_info(format, payload)?);
671            }
672            PktLineFrame::Data(_) => {
673                return Err(GitError::InvalidFormat(
674                    "upload-pack shallow update has data after flush".into(),
675                ));
676            }
677            PktLineFrame::Flush => {
678                saw_flush = true;
679                if idx + 1 != frames.len() {
680                    return Err(GitError::InvalidFormat(
681                        "upload-pack shallow update has frames after flush".into(),
682                    ));
683                }
684            }
685            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
686                return Err(GitError::InvalidFormat(
687                    "upload-pack shallow update contains a non-flush control packet".into(),
688                ));
689            }
690        }
691    }
692    if !saw_flush {
693        return Err(GitError::InvalidFormat(
694            "upload-pack shallow update missing flush".into(),
695        ));
696    }
697    Ok(entries)
698}
699
700pub fn encode_upload_pack_shallow_update(
701    entries: &[ProtocolV2FetchShallowInfo],
702) -> Result<Vec<PktLineFrame>> {
703    let mut frames = Vec::new();
704    for entry in entries {
705        let line = match entry {
706            ProtocolV2FetchShallowInfo::Shallow(oid) => format!("shallow {oid}"),
707            ProtocolV2FetchShallowInfo::Unshallow(oid) => format!("unshallow {oid}"),
708        };
709        frames.push(PktLineFrame::data(line_from_str(&line))?);
710    }
711    frames.push(PktLineFrame::Flush);
712    Ok(frames)
713}
714
715pub fn read_upload_pack_shallow_update(
716    format: ObjectFormat,
717    reader: &mut impl Read,
718) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
719    let frames = read_pkt_line_frames_until_flush(reader)?;
720    parse_upload_pack_shallow_update(format, &frames)
721}
722
723pub fn write_upload_pack_shallow_update(
724    writer: &mut impl Write,
725    entries: &[ProtocolV2FetchShallowInfo],
726) -> Result<()> {
727    for entry in entries {
728        let line = match entry {
729            ProtocolV2FetchShallowInfo::Shallow(oid) => format!("shallow {oid}"),
730            ProtocolV2FetchShallowInfo::Unshallow(oid) => format!("unshallow {oid}"),
731        };
732        write_pkt_line_payload(writer, &line_from_str(&line))?;
733    }
734    writer.write_all(b"0000")?;
735    Ok(())
736}
737
738pub fn parse_upload_pack_negotiation_request(
739    format: ObjectFormat,
740    frames: &[PktLineFrame],
741) -> Result<UploadPackNegotiationRequest> {
742    let mut request = UploadPackNegotiationRequest::default();
743    let mut terminated = false;
744    for (idx, frame) in frames.iter().enumerate() {
745        match frame {
746            PktLineFrame::Data(payload) if !terminated => {
747                let text = parse_protocol_v2_line_text("upload-pack negotiation line", payload)?;
748                if text == "done" {
749                    request.done = true;
750                    terminated = true;
751                    if idx + 1 != frames.len() {
752                        return Err(GitError::InvalidFormat(
753                            "upload-pack negotiation has frames after done".into(),
754                        ));
755                    }
756                } else if text.starts_with("have ") {
757                    request.haves.push(parse_oid_argument(
758                        format,
759                        "upload-pack have",
760                        text,
761                        "have ",
762                    )?);
763                } else {
764                    return Err(GitError::InvalidFormat(format!(
765                        "unsupported upload-pack negotiation line {text}"
766                    )));
767                }
768            }
769            PktLineFrame::Data(_) => {
770                return Err(GitError::InvalidFormat(
771                    "upload-pack negotiation has data after terminator".into(),
772                ));
773            }
774            PktLineFrame::Flush => {
775                terminated = true;
776                if idx + 1 != frames.len() {
777                    return Err(GitError::InvalidFormat(
778                        "upload-pack negotiation has frames after flush".into(),
779                    ));
780                }
781            }
782            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
783                return Err(GitError::InvalidFormat(
784                    "upload-pack negotiation contains a non-flush control packet".into(),
785                ));
786            }
787        }
788    }
789    if !terminated {
790        return Err(GitError::InvalidFormat(
791            "upload-pack negotiation missing terminator".into(),
792        ));
793    }
794    Ok(request)
795}
796
797pub fn encode_upload_pack_negotiation_request(
798    request: &UploadPackNegotiationRequest,
799) -> Result<Vec<PktLineFrame>> {
800    let mut frames = Vec::new();
801    for oid in &request.haves {
802        frames.push(PktLineFrame::data(line_from_str(&format!("have {oid}")))?);
803    }
804    if request.done {
805        frames.push(PktLineFrame::data(line_from_str("done"))?);
806    } else {
807        frames.push(PktLineFrame::Flush);
808    }
809    Ok(frames)
810}
811
812pub fn read_upload_pack_negotiation_request(
813    format: ObjectFormat,
814    reader: &mut impl Read,
815) -> Result<UploadPackNegotiationRequest> {
816    let mut frames = Vec::new();
817    loop {
818        let Some(frame) = read_pkt_line_frame(reader)? else {
819            return Err(GitError::InvalidFormat(
820                "pkt-line stream ended before upload-pack negotiation terminator".into(),
821            ));
822        };
823        let done = match &frame {
824            PktLineFrame::Flush => true,
825            PktLineFrame::Data(payload) => trim_trailing_lf(payload) == b"done",
826            _ => false,
827        };
828        frames.push(frame);
829        if done {
830            return parse_upload_pack_negotiation_request(format, &frames);
831        }
832    }
833}
834
835pub fn write_upload_pack_negotiation_request(
836    writer: &mut impl Write,
837    request: &UploadPackNegotiationRequest,
838) -> Result<()> {
839    for oid in &request.haves {
840        write_pkt_line_payload(writer, &line_from_str(&format!("have {oid}")))?;
841    }
842    if request.done {
843        write_pkt_line_payload(writer, b"done\n")?;
844    } else {
845        writer.write_all(b"0000")?;
846    }
847    Ok(())
848}
849
850pub fn parse_upload_pack_acknowledgment(
851    format: ObjectFormat,
852    payload: &[u8],
853) -> Result<UploadPackAcknowledgment> {
854    let text = parse_protocol_v2_line_text("upload-pack acknowledgment", payload)?;
855    if text == "NAK" {
856        return Ok(UploadPackAcknowledgment::Nak);
857    }
858    let Some(rest) = text.strip_prefix("ACK ") else {
859        return Err(GitError::InvalidFormat(format!(
860            "unsupported upload-pack acknowledgment {text}"
861        )));
862    };
863    let mut fields = rest.split(' ');
864    let oid = fields
865        .next()
866        .ok_or_else(|| GitError::InvalidFormat("upload-pack ACK missing object id".into()))?;
867    validate_protocol_v2_token("upload-pack ACK", oid)?;
868    let status = match fields.next() {
869        None => None,
870        Some("continue") => Some(UploadPackAckStatus::Continue),
871        Some("common") => Some(UploadPackAckStatus::Common),
872        Some("ready") => Some(UploadPackAckStatus::Ready),
873        Some(other) => {
874            return Err(GitError::InvalidFormat(format!(
875                "unsupported upload-pack ACK status {other}"
876            )));
877        }
878    };
879    if fields.next().is_some() {
880        return Err(GitError::InvalidFormat(
881            "upload-pack ACK has too many fields".into(),
882        ));
883    }
884    Ok(UploadPackAcknowledgment::Ack {
885        oid: ObjectId::from_hex(format, oid)?,
886        status,
887    })
888}
889
890pub fn encode_upload_pack_acknowledgment(
891    acknowledgment: &UploadPackAcknowledgment,
892) -> Result<Vec<u8>> {
893    let line = match acknowledgment {
894        UploadPackAcknowledgment::Nak => "NAK".to_string(),
895        UploadPackAcknowledgment::Ack { oid, status } => {
896            let mut line = format!("ACK {oid}");
897            if let Some(status) = status {
898                line.push(' ');
899                line.push_str(match status {
900                    UploadPackAckStatus::Continue => "continue",
901                    UploadPackAckStatus::Common => "common",
902                    UploadPackAckStatus::Ready => "ready",
903                });
904            }
905            line
906        }
907    };
908    Ok(line_from_str(&line))
909}
910
911pub fn read_upload_pack_acknowledgment(
912    format: ObjectFormat,
913    reader: &mut impl Read,
914) -> Result<UploadPackAcknowledgment> {
915    let Some(frame) = read_pkt_line_frame(reader)? else {
916        return Err(GitError::InvalidFormat(
917            "pkt-line stream ended before upload-pack acknowledgment".into(),
918        ));
919    };
920    match frame {
921        PktLineFrame::Data(payload) => parse_upload_pack_acknowledgment(format, &payload),
922        _ => Err(GitError::InvalidFormat(
923            "upload-pack acknowledgment must be a data packet".into(),
924        )),
925    }
926}
927
928pub fn write_upload_pack_acknowledgment(
929    writer: &mut impl Write,
930    acknowledgment: &UploadPackAcknowledgment,
931) -> Result<()> {
932    write_pkt_line_payload(writer, &encode_upload_pack_acknowledgment(acknowledgment)?)
933}
934
935pub fn parse_upload_pack_packfile_response(
936    format: ObjectFormat,
937    frames: &[PktLineFrame],
938) -> Result<UploadPackPackfileResponse> {
939    let mut response = UploadPackPackfileResponse::default();
940    let mut in_sideband = false;
941    let mut saw_flush = false;
942    for (idx, frame) in frames.iter().enumerate() {
943        match frame {
944            PktLineFrame::Data(payload) if !saw_flush => {
945                if !in_sideband
946                    && (trim_trailing_lf(payload) == b"NAK" || payload.starts_with(b"ACK "))
947                {
948                    response
949                        .acknowledgments
950                        .push(parse_upload_pack_acknowledgment(format, payload)?);
951                    continue;
952                }
953                in_sideband = true;
954                response.sideband.push(parse_sideband_packet(payload)?);
955            }
956            PktLineFrame::Data(_) => {
957                return Err(GitError::InvalidFormat(
958                    "upload-pack packfile response has data after flush".into(),
959                ));
960            }
961            PktLineFrame::Flush => {
962                saw_flush = true;
963                if idx + 1 != frames.len() {
964                    return Err(GitError::InvalidFormat(
965                        "upload-pack packfile response has frames after flush".into(),
966                    ));
967                }
968            }
969            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
970                return Err(GitError::InvalidFormat(
971                    "upload-pack packfile response contains a non-flush control packet".into(),
972                ));
973            }
974        }
975    }
976    if !saw_flush {
977        return Err(GitError::InvalidFormat(
978            "upload-pack packfile response missing flush".into(),
979        ));
980    }
981    Ok(response)
982}
983
984pub fn encode_upload_pack_packfile_response(
985    response: &UploadPackPackfileResponse,
986) -> Result<Vec<PktLineFrame>> {
987    let mut frames = Vec::new();
988    for acknowledgment in &response.acknowledgments {
989        frames.push(PktLineFrame::data(encode_upload_pack_acknowledgment(
990            acknowledgment,
991        )?)?);
992    }
993    for packet in &response.sideband {
994        frames.push(PktLineFrame::data(encode_sideband_packet(packet)?)?);
995    }
996    frames.push(PktLineFrame::Flush);
997    Ok(frames)
998}
999
1000pub fn read_upload_pack_packfile_response(
1001    format: ObjectFormat,
1002    reader: &mut impl Read,
1003) -> Result<UploadPackPackfileResponse> {
1004    let frames = read_pkt_line_frames_until_flush(reader)?;
1005    parse_upload_pack_packfile_response(format, &frames)
1006}
1007
1008pub fn write_upload_pack_packfile_response(
1009    writer: &mut impl Write,
1010    response: &UploadPackPackfileResponse,
1011) -> Result<()> {
1012    for acknowledgment in &response.acknowledgments {
1013        write_upload_pack_acknowledgment(writer, acknowledgment)?;
1014    }
1015    for packet in &response.sideband {
1016        write_sideband_packet(writer, packet)?;
1017    }
1018    writer.write_all(b"0000")?;
1019    Ok(())
1020}
1021
1022pub fn demux_upload_pack_packfile_response(
1023    response: &UploadPackPackfileResponse,
1024) -> Result<SideBandDemux> {
1025    demux_sideband_packets(&response.sideband)
1026}
1027
1028pub fn parse_upload_pack_raw_packfile_response(
1029    format: ObjectFormat,
1030    input: &[u8],
1031) -> Result<UploadPackRawPackfileResponse> {
1032    let mut response = UploadPackRawPackfileResponse::default();
1033    let mut offset = 0usize;
1034    while offset < input.len() {
1035        match PktLineFrame::parse(&input[offset..]) {
1036            Ok((PktLineFrame::Data(payload), consumed)) => {
1037                let trimmed = trim_trailing_lf(&payload);
1038                if trimmed == b"NAK" || trimmed.starts_with(b"ACK ") {
1039                    response
1040                        .acknowledgments
1041                        .push(parse_upload_pack_acknowledgment(format, &payload)?);
1042                    offset += consumed;
1043                    continue;
1044                }
1045                return Err(GitError::InvalidFormat(
1046                    "upload-pack raw packfile response has non-ack pkt-line before packfile".into(),
1047                ));
1048            }
1049            Ok((PktLineFrame::Flush | PktLineFrame::Delimiter | PktLineFrame::ResponseEnd, _)) => {
1050                return Err(GitError::InvalidFormat(
1051                    "upload-pack raw packfile response contains a control packet".into(),
1052                ));
1053            }
1054            Err(_) if input[offset..].starts_with(b"PACK") => break,
1055            Err(err) => return Err(err),
1056        }
1057    }
1058    response.packfile = input[offset..].to_vec();
1059    if response.packfile.is_empty() {
1060        return Err(GitError::InvalidFormat(
1061            "upload-pack raw packfile response missing packfile".into(),
1062        ));
1063    }
1064    if !response.packfile.starts_with(b"PACK") {
1065        return Err(GitError::InvalidFormat(
1066            "upload-pack raw packfile response packfile must start with PACK".into(),
1067        ));
1068    }
1069    Ok(response)
1070}
1071
1072pub fn encode_upload_pack_raw_packfile_response(
1073    response: &UploadPackRawPackfileResponse,
1074) -> Result<Vec<u8>> {
1075    if response.packfile.is_empty() {
1076        return Err(GitError::InvalidFormat(
1077            "upload-pack raw packfile response missing packfile".into(),
1078        ));
1079    }
1080    if !response.packfile.starts_with(b"PACK") {
1081        return Err(GitError::InvalidFormat(
1082            "upload-pack raw packfile response packfile must start with PACK".into(),
1083        ));
1084    }
1085    let mut out = Vec::new();
1086    for acknowledgment in &response.acknowledgments {
1087        write_pkt_line_payload(
1088            &mut out,
1089            &encode_upload_pack_acknowledgment(acknowledgment)?,
1090        )?;
1091    }
1092    out.extend_from_slice(&response.packfile);
1093    Ok(out)
1094}
1095
1096pub fn read_upload_pack_raw_packfile_response(
1097    format: ObjectFormat,
1098    reader: &mut impl Read,
1099) -> Result<UploadPackRawPackfileResponse> {
1100    let mut input = Vec::new();
1101    reader.read_to_end(&mut input)?;
1102    parse_upload_pack_raw_packfile_response(format, &input)
1103}
1104
1105pub fn read_upload_pack_raw_packfile_response_header(
1106    format: ObjectFormat,
1107    reader: &mut impl Read,
1108) -> Result<UploadPackRawPackfileResponseHeader> {
1109    let mut acknowledgments = Vec::new();
1110    loop {
1111        let mut header = [0u8; 4];
1112        reader.read_exact(&mut header)?;
1113        if &header == b"PACK" {
1114            return Ok(UploadPackRawPackfileResponseHeader {
1115                acknowledgments,
1116                pack_prefix: header.to_vec(),
1117            });
1118        }
1119        let len = parse_pkt_len(&header)?;
1120        let payload = match len {
1121            0..=2 => {
1122                return Err(GitError::InvalidFormat(
1123                    "upload-pack raw packfile response contains a control packet".into(),
1124                ));
1125            }
1126            3 => {
1127                return Err(GitError::InvalidFormat(
1128                    "reserved pkt-line length 0003".into(),
1129                ));
1130            }
1131            4..=PKT_LINE_MAX_LEN => {
1132                let mut payload = vec![0; len - 4];
1133                reader.read_exact(&mut payload)?;
1134                payload
1135            }
1136            _ => {
1137                return Err(GitError::InvalidFormat(format!(
1138                    "pkt-line length exceeds {PKT_LINE_MAX_LEN}: {len}"
1139                )));
1140            }
1141        };
1142        trace_packet_read_payload(&payload);
1143        let trimmed = trim_trailing_lf(&payload);
1144        if trimmed == b"NAK" || trimmed.starts_with(b"ACK ") {
1145            acknowledgments.push(parse_upload_pack_acknowledgment(format, &payload)?);
1146            continue;
1147        }
1148        return Err(GitError::InvalidFormat(
1149            "upload-pack raw packfile response has non-ack pkt-line before packfile".into(),
1150        ));
1151    }
1152}
1153
1154pub fn read_upload_pack_shallow_info_section(
1155    format: ObjectFormat,
1156    reader: &mut impl Read,
1157) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
1158    let mut entries = Vec::new();
1159    loop {
1160        let Some(frame) = read_pkt_line_frame(reader)? else {
1161            return Err(GitError::InvalidFormat(
1162                "upload-pack shallow-info section ended before flush".into(),
1163            ));
1164        };
1165        match frame {
1166            PktLineFrame::Data(payload) => {
1167                entries.push(parse_fetch_shallow_info(format, &payload)?)
1168            }
1169            PktLineFrame::Flush => return Ok(entries),
1170            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
1171                return Err(GitError::InvalidFormat(
1172                    "upload-pack shallow-info section contains a non-flush control packet".into(),
1173                ));
1174            }
1175        }
1176    }
1177}
1178
1179pub fn read_upload_pack_shallow_info_and_raw_packfile_response_header(
1180    format: ObjectFormat,
1181    reader: &mut impl Read,
1182) -> Result<(
1183    Vec<ProtocolV2FetchShallowInfo>,
1184    UploadPackRawPackfileResponseHeader,
1185)> {
1186    let shallow = read_upload_pack_shallow_info_section(format, reader)?;
1187    let raw = read_upload_pack_raw_packfile_response_header(format, reader)?;
1188    Ok((shallow, raw))
1189}
1190
1191pub fn write_upload_pack_raw_packfile_response(
1192    writer: &mut impl Write,
1193    response: &UploadPackRawPackfileResponse,
1194) -> Result<()> {
1195    if response.packfile.is_empty() {
1196        return Err(GitError::InvalidFormat(
1197            "upload-pack raw packfile response missing packfile".into(),
1198        ));
1199    }
1200    if !response.packfile.starts_with(b"PACK") {
1201        return Err(GitError::InvalidFormat(
1202            "upload-pack raw packfile response packfile must start with PACK".into(),
1203        ));
1204    }
1205    for acknowledgment in &response.acknowledgments {
1206        write_upload_pack_acknowledgment(writer, acknowledgment)?;
1207    }
1208    writer.write_all(&response.packfile)?;
1209    Ok(())
1210}
1211
1212/// Parse the smart-HTTP/SSH v0 *shallow-info* section that precedes the packfile
1213/// when the upload-pack request carried `shallow`/`deepen`/`deepen-since`/
1214/// `deepen-not` arguments.
1215///
1216/// The section is zero or more `shallow <oid>` / `unshallow <oid>` pkt-lines
1217/// terminated by a flush-pkt; git always emits it (even empty, as a bare flush)
1218/// when the request was a deepen request, and never emits it otherwise. Returns
1219/// the parsed entries and the number of bytes consumed (through the flush) so the
1220/// caller can continue parsing the trailing acknowledgments + packfile from
1221/// `&input[consumed..]` (see [`parse_upload_pack_shallow_info_and_raw_packfile_response`]).
1222pub fn parse_upload_pack_shallow_info_section(
1223    format: ObjectFormat,
1224    input: &[u8],
1225) -> Result<(Vec<ProtocolV2FetchShallowInfo>, usize)> {
1226    let mut entries = Vec::new();
1227    let mut offset = 0usize;
1228    loop {
1229        let (frame, consumed) = PktLineFrame::parse(&input[offset..])?;
1230        offset += consumed;
1231        match frame {
1232            PktLineFrame::Data(payload) => {
1233                entries.push(parse_fetch_shallow_info(format, &payload)?)
1234            }
1235            PktLineFrame::Flush => return Ok((entries, offset)),
1236            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
1237                return Err(GitError::InvalidFormat(
1238                    "upload-pack shallow-info section contains a non-flush control packet".into(),
1239                ));
1240            }
1241        }
1242    }
1243}
1244
1245/// Parse a raw upload-pack response that begins with a *shallow-info* section,
1246/// i.e. the response to a deepen request.
1247///
1248/// This is [`parse_upload_pack_raw_packfile_response`] preceded by the
1249/// shallow-info section ([`parse_upload_pack_shallow_info_section`]): it returns
1250/// the `shallow`/`unshallow` entries the server reported alongside the parsed
1251/// acknowledgments + raw packfile. Use it only when the request carried a
1252/// `shallow`/`deepen`/`deepen-since`/`deepen-not` argument; for a plain (non-deepen)
1253/// request the response has no leading shallow-info section and
1254/// [`parse_upload_pack_raw_packfile_response`] must be used instead.
1255pub fn parse_upload_pack_shallow_info_and_raw_packfile_response(
1256    format: ObjectFormat,
1257    input: &[u8],
1258) -> Result<(
1259    Vec<ProtocolV2FetchShallowInfo>,
1260    UploadPackRawPackfileResponse,
1261)> {
1262    let (shallow, consumed) = parse_upload_pack_shallow_info_section(format, input)?;
1263    let response = parse_upload_pack_raw_packfile_response(format, &input[consumed..])?;
1264    Ok((shallow, response))
1265}
1266
1267/// Read a raw upload-pack response that begins with a *shallow-info* section from
1268/// `reader`, returning the `shallow`/`unshallow` entries and the parsed
1269/// acknowledgments + raw packfile.
1270///
1271/// The reader counterpart of
1272/// [`parse_upload_pack_shallow_info_and_raw_packfile_response`]; see it for when
1273/// this applies.
1274pub fn read_upload_pack_shallow_info_and_raw_packfile_response(
1275    format: ObjectFormat,
1276    reader: &mut impl Read,
1277) -> Result<(
1278    Vec<ProtocolV2FetchShallowInfo>,
1279    UploadPackRawPackfileResponse,
1280)> {
1281    let (shallow, header) =
1282        read_upload_pack_shallow_info_and_raw_packfile_response_header(format, reader)?;
1283    let mut packfile = header.pack_prefix;
1284    reader.read_to_end(&mut packfile)?;
1285    Ok((
1286        shallow,
1287        UploadPackRawPackfileResponse {
1288            acknowledgments: header.acknowledgments,
1289            packfile,
1290        },
1291    ))
1292}
1293fn set_upload_pack_flag(value: &mut bool, capability: &Capability) -> Result<()> {
1294    reject_capability_value("upload-pack capability", capability)?;
1295    if *value {
1296        return Err(GitError::InvalidFormat(format!(
1297            "upload-pack has duplicate {} capability",
1298            capability.name
1299        )));
1300    }
1301    *value = true;
1302    Ok(())
1303}
1304
1305fn push_upload_pack_flag(capabilities: &mut Vec<Capability>, name: &str, enabled: bool) {
1306    if enabled {
1307        capabilities.push(Capability {
1308            name: name.into(),
1309            value: None,
1310        });
1311    }
1312}
1313
1314fn is_known_upload_pack_capability(name: &str) -> bool {
1315    matches!(
1316        name,
1317        "multi_ack"
1318            | "multi_ack_detailed"
1319            | "no-done"
1320            | "thin-pack"
1321            | "side-band"
1322            | "side-band-64k"
1323            | "ofs-delta"
1324            | "shallow"
1325            | "deepen-since"
1326            | "deepen-not"
1327            | "include-tag"
1328            | "no-progress"
1329            | "allow-tip-sha1-in-want"
1330            | "allow-reachable-sha1-in-want"
1331            | "filter"
1332            | "agent"
1333            | "object-format"
1334            | "symref"
1335    )
1336}
1337
1338fn is_upload_pack_flag_capability(name: &str) -> bool {
1339    matches!(
1340        name,
1341        "multi_ack"
1342            | "multi_ack_detailed"
1343            | "no-done"
1344            | "thin-pack"
1345            | "side-band"
1346            | "side-band-64k"
1347            | "ofs-delta"
1348            | "shallow"
1349            | "deepen-since"
1350            | "deepen-not"
1351            | "include-tag"
1352            | "no-progress"
1353            | "allow-tip-sha1-in-want"
1354            | "allow-reachable-sha1-in-want"
1355            | "filter"
1356    )
1357}