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