Skip to main content

sley_protocol/
v2.rs

1use sley_core::{Capability, GitError, ObjectFormat, ObjectId, Result};
2use std::io::{Read, Write};
3
4use crate::pktline::{
5    PktLineFrame, ProtocolVersion, line, line_from_str, parse_oid_argument,
6    parse_protocol_v2_line_text, read_pkt_line_frame, read_pkt_line_frames_until_flush,
7    read_pkt_line_frames_until_response_end, trace_packet_read_payload, trim_trailing_lf,
8    validate_capability_name, validate_protocol_v2_line, validate_protocol_v2_token,
9    write_pkt_line_frame, write_pkt_line_payload,
10};
11use crate::sideband::{
12    SideBandChannel, SideBandDemux, SideBandPacket, encode_sideband_packet,
13    parse_and_demux_sideband_packets, parse_sideband_packet, write_sideband_payload,
14};
15use crate::v0::{RefAdvertisement, RefAdvertisementSet, TransportHandshake};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ProtocolV2CommandRequest {
19    pub command: String,
20    pub capabilities: Vec<Capability>,
21    pub arguments: Vec<Vec<u8>>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum ProtocolV2Request {
26    Command(ProtocolV2CommandRequest),
27    Done,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum ProtocolV2Command {
32    LsRefs(ProtocolV2LsRefsRequest),
33    Fetch(ProtocolV2FetchRequest),
34    ObjectInfo(ProtocolV2ObjectInfoRequest),
35    Unknown(ProtocolV2CommandRequest),
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum ProtocolV2SessionRequest {
40    Command(ProtocolV2Command),
41    Done,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Default)]
45pub struct ProtocolV2CommandOptions {
46    pub agent: Option<String>,
47    pub object_format: Option<ObjectFormat>,
48    pub server_options: Vec<String>,
49    pub extra: Vec<Capability>,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Default)]
53pub struct ProtocolV2FetchFeatures {
54    pub shallow: bool,
55    pub wait_for_done: bool,
56    pub filter: bool,
57    pub ref_in_want: bool,
58    pub sideband_all: bool,
59    pub packfile_uris: bool,
60    pub unknown: Vec<String>,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Default)]
64pub struct ProtocolV2LsRefsFeatures {
65    pub unborn: bool,
66    pub unknown: Vec<String>,
67}
68
69impl ProtocolV2CommandRequest {
70    pub fn new(command: impl Into<String>) -> Result<Self> {
71        let command = command.into();
72        validate_capability_name(&command)?;
73        Ok(Self {
74            command,
75            capabilities: Vec::new(),
76            arguments: Vec::new(),
77        })
78    }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Default)]
82pub struct ProtocolV2LsRefsRequest {
83    pub peel: bool,
84    pub symrefs: bool,
85    pub unborn: bool,
86    pub ref_prefixes: Vec<String>,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct ProtocolV2LsRefsRef {
91    pub oid: ObjectId,
92    pub name: String,
93    pub peeled: Option<ObjectId>,
94    pub symref_target: Option<String>,
95    pub attributes: Vec<String>,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum ProtocolV2LsRefsRecord {
100    Ref(ProtocolV2LsRefsRef),
101    Unborn {
102        name: String,
103        symref_target: Option<String>,
104        attributes: Vec<String>,
105    },
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Default)]
109pub struct ProtocolV2FetchRequest {
110    pub wants: Vec<ObjectId>,
111    pub want_refs: Vec<String>,
112    pub haves: Vec<ObjectId>,
113    pub shallow: Vec<ObjectId>,
114    pub deepen: Option<u32>,
115    pub deepen_since: Option<u64>,
116    pub deepen_not: Vec<String>,
117    pub deepen_relative: bool,
118    pub filter: Option<String>,
119    pub packfile_uris: Option<String>,
120    pub thin_pack: bool,
121    pub no_progress: bool,
122    pub include_tag: bool,
123    pub ofs_delta: bool,
124    pub sideband_all: bool,
125    pub wait_for_done: bool,
126    pub done: bool,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub enum ProtocolV2FetchAcknowledgment {
131    Nak,
132    Ack(ObjectId),
133    Ready,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub enum ProtocolV2FetchShallowInfo {
138    Shallow(ObjectId),
139    Unshallow(ObjectId),
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct ProtocolV2FetchWantedRef {
144    pub oid: ObjectId,
145    pub name: String,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct ProtocolV2FetchPackfileUri {
150    pub pack_hash: ObjectId,
151    pub uri: String,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum ProtocolV2FetchResponseSection {
156    Acknowledgments(Vec<ProtocolV2FetchAcknowledgment>),
157    ShallowInfo(Vec<ProtocolV2FetchShallowInfo>),
158    WantedRefs(Vec<ProtocolV2FetchWantedRef>),
159    PackfileUris(Vec<ProtocolV2FetchPackfileUri>),
160    Packfile(Vec<Vec<u8>>),
161    Unknown { name: String, lines: Vec<Vec<u8>> },
162}
163
164#[derive(Debug, Clone, PartialEq, Eq, Default)]
165pub struct ProtocolV2FetchSidebandAllResponse {
166    pub sections: Vec<ProtocolV2FetchResponseSection>,
167    pub progress: Vec<Vec<u8>>,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Default)]
171pub struct ProtocolV2FetchResponseHeader {
172    pub sections: Vec<ProtocolV2FetchResponseSection>,
173    pub has_packfile: bool,
174}
175
176/// The acknowledgment phase of a multi-round protocol-v2 fetch.
177///
178/// When `has_following_sections` is true, the delimiter after
179/// `acknowledgments` has already been consumed and the reader is positioned at
180/// the next response section. Otherwise the response ended with a flush and
181/// the client must send another negotiation request.
182#[derive(Debug, Clone, PartialEq, Eq, Default)]
183pub struct ProtocolV2FetchNegotiationResponse {
184    pub acknowledgments: Vec<ProtocolV2FetchAcknowledgment>,
185    pub has_following_sections: bool,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Default)]
189pub struct ProtocolV2ObjectInfoRequest {
190    pub size: bool,
191    pub oids: Vec<ObjectId>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct ProtocolV2ObjectInfoRecord {
196    pub oid: ObjectId,
197    pub size: u64,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Default)]
201pub struct ProtocolV2ObjectInfoResponse {
202    pub size: bool,
203    pub records: Vec<ProtocolV2ObjectInfoRecord>,
204}
205
206impl ProtocolV2LsRefsRequest {
207    pub fn from_command_request(request: &ProtocolV2CommandRequest) -> Result<Self> {
208        if request.command != "ls-refs" {
209            return Err(GitError::InvalidFormat(format!(
210                "expected ls-refs command, got {}",
211                request.command
212            )));
213        }
214        let mut out = Self::default();
215        for argument in &request.arguments {
216            let text = std::str::from_utf8(argument)
217                .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
218            match text {
219                "peel" => out.peel = true,
220                "symrefs" => out.symrefs = true,
221                "unborn" => out.unborn = true,
222                value if value.starts_with("ref-prefix ") => {
223                    let prefix = value
224                        .strip_prefix("ref-prefix ")
225                        .ok_or_else(|| GitError::InvalidFormat("invalid ref-prefix".into()))?;
226                    validate_protocol_v2_token("ls-refs ref-prefix", prefix)?;
227                    out.ref_prefixes.push(prefix.to_string());
228                }
229                other => {
230                    return Err(GitError::InvalidFormat(format!(
231                        "unsupported ls-refs argument {other}"
232                    )));
233                }
234            }
235        }
236        Ok(out)
237    }
238
239    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
240        let mut request = ProtocolV2CommandRequest::new("ls-refs")?;
241        if self.peel {
242            request.arguments.push(b"peel".to_vec());
243        }
244        if self.symrefs {
245            request.arguments.push(b"symrefs".to_vec());
246        }
247        if self.unborn {
248            request.arguments.push(b"unborn".to_vec());
249        }
250        for prefix in &self.ref_prefixes {
251            validate_protocol_v2_token("ls-refs ref-prefix", prefix)?;
252            request
253                .arguments
254                .push(format!("ref-prefix {prefix}").into_bytes());
255        }
256        Ok(request)
257    }
258}
259
260impl ProtocolV2FetchRequest {
261    pub fn from_command_request(
262        format: ObjectFormat,
263        request: &ProtocolV2CommandRequest,
264    ) -> Result<Self> {
265        if request.command != "fetch" {
266            return Err(GitError::InvalidFormat(format!(
267                "expected fetch command, got {}",
268                request.command
269            )));
270        }
271        let mut out = Self::default();
272        for argument in &request.arguments {
273            let text = std::str::from_utf8(argument)
274                .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
275            match text {
276                "thin-pack" => out.thin_pack = true,
277                "no-progress" => out.no_progress = true,
278                "include-tag" => out.include_tag = true,
279                "ofs-delta" => out.ofs_delta = true,
280                "sideband-all" => out.sideband_all = true,
281                "wait-for-done" => out.wait_for_done = true,
282                "deepen-relative" => out.deepen_relative = true,
283                "done" => out.done = true,
284                value if value.starts_with("want ") => {
285                    out.wants
286                        .push(parse_oid_argument(format, "fetch want", value, "want ")?);
287                }
288                value if value.starts_with("want-ref ") => {
289                    let name = value
290                        .strip_prefix("want-ref ")
291                        .ok_or_else(|| GitError::InvalidFormat("invalid fetch want-ref".into()))?;
292                    validate_protocol_v2_token("fetch want-ref", name)?;
293                    out.want_refs.push(name.to_string());
294                }
295                value if value.starts_with("have ") => {
296                    out.haves
297                        .push(parse_oid_argument(format, "fetch have", value, "have ")?);
298                }
299                value if value.starts_with("shallow ") => {
300                    out.shallow.push(parse_oid_argument(
301                        format,
302                        "fetch shallow",
303                        value,
304                        "shallow ",
305                    )?);
306                }
307                value if value.starts_with("deepen ") => {
308                    if out.deepen.is_some() {
309                        return Err(GitError::InvalidFormat(
310                            "fetch request has duplicate deepen".into(),
311                        ));
312                    }
313                    out.deepen = Some(parse_u32_argument("fetch deepen", value, "deepen ")?);
314                }
315                value if value.starts_with("deepen-since ") => {
316                    if out.deepen_since.is_some() {
317                        return Err(GitError::InvalidFormat(
318                            "fetch request has duplicate deepen-since".into(),
319                        ));
320                    }
321                    out.deepen_since = Some(parse_u64_argument(
322                        "fetch deepen-since",
323                        value,
324                        "deepen-since ",
325                    )?);
326                }
327                value if value.starts_with("deepen-not ") => {
328                    let name = value.strip_prefix("deepen-not ").ok_or_else(|| {
329                        GitError::InvalidFormat("invalid fetch deepen-not".into())
330                    })?;
331                    validate_protocol_v2_token("fetch deepen-not", name)?;
332                    out.deepen_not.push(name.to_string());
333                }
334                value if value.starts_with("filter ") => {
335                    if out.filter.is_some() {
336                        return Err(GitError::InvalidFormat(
337                            "fetch request has duplicate filter".into(),
338                        ));
339                    }
340                    let filter = value
341                        .strip_prefix("filter ")
342                        .ok_or_else(|| GitError::InvalidFormat("invalid fetch filter".into()))?;
343                    validate_protocol_v2_token("fetch filter", filter)?;
344                    out.filter = Some(filter.to_string());
345                }
346                value if value.starts_with("packfile-uris ") => {
347                    if out.packfile_uris.is_some() {
348                        return Err(GitError::InvalidFormat(
349                            "fetch request has duplicate packfile-uris".into(),
350                        ));
351                    }
352                    let protocols = value.strip_prefix("packfile-uris ").ok_or_else(|| {
353                        GitError::InvalidFormat("invalid fetch packfile-uris".into())
354                    })?;
355                    validate_protocol_v2_token("fetch packfile-uris", protocols)?;
356                    out.packfile_uris = Some(protocols.to_string());
357                }
358                other => {
359                    return Err(GitError::InvalidFormat(format!(
360                        "unsupported fetch argument {other}"
361                    )));
362                }
363            }
364        }
365        Ok(out)
366    }
367
368    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
369        let mut request = ProtocolV2CommandRequest::new("fetch")?;
370        for oid in &self.wants {
371            request.arguments.push(format!("want {oid}").into_bytes());
372        }
373        for name in &self.want_refs {
374            validate_protocol_v2_token("fetch want-ref", name)?;
375            request
376                .arguments
377                .push(format!("want-ref {name}").into_bytes());
378        }
379        for oid in &self.haves {
380            request.arguments.push(format!("have {oid}").into_bytes());
381        }
382        for oid in &self.shallow {
383            request
384                .arguments
385                .push(format!("shallow {oid}").into_bytes());
386        }
387        if let Some(deepen) = self.deepen {
388            if deepen == 0 {
389                return Err(GitError::InvalidFormat(
390                    "fetch deepen must be positive".into(),
391                ));
392            }
393            request
394                .arguments
395                .push(format!("deepen {deepen}").into_bytes());
396        }
397        if let Some(deepen_since) = self.deepen_since {
398            request
399                .arguments
400                .push(format!("deepen-since {deepen_since}").into_bytes());
401        }
402        for name in &self.deepen_not {
403            validate_protocol_v2_token("fetch deepen-not", name)?;
404            request
405                .arguments
406                .push(format!("deepen-not {name}").into_bytes());
407        }
408        if self.deepen_relative {
409            request.arguments.push(b"deepen-relative".to_vec());
410        }
411        if let Some(filter) = &self.filter {
412            validate_protocol_v2_token("fetch filter", filter)?;
413            request
414                .arguments
415                .push(format!("filter {filter}").into_bytes());
416        }
417        if let Some(protocols) = &self.packfile_uris {
418            validate_protocol_v2_token("fetch packfile-uris", protocols)?;
419            request
420                .arguments
421                .push(format!("packfile-uris {protocols}").into_bytes());
422        }
423        if self.thin_pack {
424            request.arguments.push(b"thin-pack".to_vec());
425        }
426        if self.no_progress {
427            request.arguments.push(b"no-progress".to_vec());
428        }
429        if self.include_tag {
430            request.arguments.push(b"include-tag".to_vec());
431        }
432        if self.ofs_delta {
433            request.arguments.push(b"ofs-delta".to_vec());
434        }
435        if self.sideband_all {
436            request.arguments.push(b"sideband-all".to_vec());
437        }
438        if self.wait_for_done {
439            request.arguments.push(b"wait-for-done".to_vec());
440        }
441        if self.done {
442            request.arguments.push(b"done".to_vec());
443        }
444        Ok(request)
445    }
446}
447
448impl ProtocolV2ObjectInfoRequest {
449    pub fn from_command_request(
450        format: ObjectFormat,
451        request: &ProtocolV2CommandRequest,
452    ) -> Result<Self> {
453        if request.command != "object-info" {
454            return Err(GitError::InvalidFormat(format!(
455                "expected object-info command, got {}",
456                request.command
457            )));
458        }
459        let mut out = Self::default();
460        for argument in &request.arguments {
461            let text = parse_protocol_v2_line_text("object-info request argument", argument)?;
462            if text == "size" {
463                if out.size {
464                    return Err(GitError::InvalidFormat(
465                        "object-info request has duplicate size argument".into(),
466                    ));
467                }
468                out.size = true;
469            } else if text.starts_with("oid ") {
470                out.oids
471                    .push(parse_oid_argument(format, "object-info oid", text, "oid ")?);
472            } else {
473                return Err(GitError::InvalidFormat(format!(
474                    "unsupported object-info request argument {text}"
475                )));
476            }
477        }
478        if !out.size {
479            return Err(GitError::InvalidFormat(
480                "object-info request is missing size argument".into(),
481            ));
482        }
483        if out.oids.is_empty() {
484            return Err(GitError::InvalidFormat(
485                "object-info request is missing object ids".into(),
486            ));
487        }
488        Ok(out)
489    }
490
491    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
492        if !self.size {
493            return Err(GitError::InvalidFormat(
494                "object-info request is missing size argument".into(),
495            ));
496        }
497        if self.oids.is_empty() {
498            return Err(GitError::InvalidFormat(
499                "object-info request is missing object ids".into(),
500            ));
501        }
502        let mut request = ProtocolV2CommandRequest::new("object-info")?;
503        request.arguments.push(b"size".to_vec());
504        for oid in &self.oids {
505            request.arguments.push(format!("oid {oid}").into_bytes());
506        }
507        Ok(request)
508    }
509}
510
511pub fn parse_protocol_v2_advertisement(frames: &[PktLineFrame]) -> Result<TransportHandshake> {
512    let Some((first, rest)) = frames.split_first() else {
513        return Err(GitError::InvalidFormat(
514            "protocol v2 advertisement is empty".into(),
515        ));
516    };
517    match first {
518        PktLineFrame::Data(payload) if trim_trailing_lf(payload) == b"version 2" => {}
519        PktLineFrame::Data(_) => {
520            return Err(GitError::InvalidFormat(
521                "protocol v2 advertisement missing version line".into(),
522            ));
523        }
524        _ => {
525            return Err(GitError::InvalidFormat(
526                "protocol v2 advertisement must start with a data line".into(),
527            ));
528        }
529    }
530
531    let mut capabilities = Vec::new();
532    let mut saw_flush = false;
533    for (idx, frame) in rest.iter().enumerate() {
534        match frame {
535            PktLineFrame::Data(payload) => {
536                if saw_flush {
537                    return Err(GitError::InvalidFormat(
538                        "protocol v2 advertisement has data after flush".into(),
539                    ));
540                }
541                capabilities.push(parse_protocol_v2_capability_line(payload)?);
542            }
543            PktLineFrame::Flush => {
544                saw_flush = true;
545                if idx + 1 != rest.len() {
546                    return Err(GitError::InvalidFormat(
547                        "protocol v2 advertisement has frames after flush".into(),
548                    ));
549                }
550            }
551            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
552                return Err(GitError::InvalidFormat(
553                    "protocol v2 advertisement contains a non-flush control packet".into(),
554                ));
555            }
556        }
557    }
558    if !saw_flush {
559        return Err(GitError::InvalidFormat(
560            "protocol v2 advertisement missing flush".into(),
561        ));
562    }
563
564    Ok(TransportHandshake {
565        protocol: ProtocolVersion::V2,
566        capabilities,
567    })
568}
569
570pub fn encode_protocol_v2_advertisement(
571    handshake: &TransportHandshake,
572) -> Result<Vec<PktLineFrame>> {
573    if handshake.protocol != ProtocolVersion::V2 {
574        return Err(GitError::InvalidFormat(
575            "protocol v2 advertisement requires a v2 handshake".into(),
576        ));
577    }
578    let mut frames = vec![PktLineFrame::data(line_from_str("version 2"))?];
579    for capability in &handshake.capabilities {
580        frames.push(PktLineFrame::data(line(encode_protocol_v2_capability(
581            capability,
582        )?))?);
583    }
584    frames.push(PktLineFrame::Flush);
585    Ok(frames)
586}
587
588pub fn read_protocol_v2_advertisement(reader: &mut impl Read) -> Result<TransportHandshake> {
589    let frames = read_pkt_line_frames_until_flush(reader)?;
590    parse_protocol_v2_advertisement(&frames)
591}
592
593pub fn write_protocol_v2_advertisement(
594    writer: &mut impl Write,
595    handshake: &TransportHandshake,
596) -> Result<()> {
597    if handshake.protocol != ProtocolVersion::V2 {
598        return Err(GitError::InvalidFormat(
599            "protocol v2 advertisement requires a v2 handshake".into(),
600        ));
601    }
602    write_pkt_line_payload(writer, b"version 2\n")?;
603    for capability in &handshake.capabilities {
604        write_pkt_line_payload(writer, &line(encode_protocol_v2_capability(capability)?))?;
605    }
606    writer.write_all(b"0000")?;
607    Ok(())
608}
609
610/// Trace a previously discovered v2 advertisement as incoming packets at the
611/// current logical protocol consumer. Smart HTTP discovers capabilities in its
612/// remote-helper phase, then forwards that handshake to `fetch`; replaying the
613/// packet trace at that in-process boundary preserves Git's observable
614/// `fetch< version 2` trace without putting a second advertisement on the HTTP
615/// RPC wire.
616pub fn trace_protocol_v2_advertisement_read(handshake: &TransportHandshake) -> Result<()> {
617    for frame in encode_protocol_v2_advertisement(handshake)? {
618        match frame {
619            PktLineFrame::Data(payload) => trace_packet_read_payload(&payload),
620            PktLineFrame::Flush => trace_packet_read_payload(b"0000"),
621            PktLineFrame::Delimiter => trace_packet_read_payload(b"0001"),
622            PktLineFrame::ResponseEnd => trace_packet_read_payload(b"0002"),
623        }
624    }
625    Ok(())
626}
627
628pub fn parse_protocol_v2_command_request(
629    frames: &[PktLineFrame],
630) -> Result<ProtocolV2CommandRequest> {
631    let Some((first, rest)) = frames.split_first() else {
632        return Err(GitError::InvalidFormat(
633            "protocol v2 command request is empty".into(),
634        ));
635    };
636    let command = match first {
637        PktLineFrame::Data(payload) => parse_protocol_v2_command_line(payload)?,
638        _ => {
639            return Err(GitError::InvalidFormat(
640                "protocol v2 command request must start with a command line".into(),
641            ));
642        }
643    };
644
645    let mut capabilities = Vec::new();
646    let mut arguments = Vec::new();
647    let mut in_arguments = false;
648    let mut saw_flush = false;
649    for (idx, frame) in rest.iter().enumerate() {
650        match frame {
651            PktLineFrame::Data(payload) if !in_arguments => {
652                if saw_flush {
653                    return Err(GitError::InvalidFormat(
654                        "protocol v2 command request has data after flush".into(),
655                    ));
656                }
657                capabilities.push(parse_protocol_v2_capability_line(payload)?);
658            }
659            PktLineFrame::Data(payload) => {
660                if saw_flush {
661                    return Err(GitError::InvalidFormat(
662                        "protocol v2 command request has data after flush".into(),
663                    ));
664                }
665                let argument = trim_trailing_lf(payload);
666                if argument.is_empty() {
667                    return Err(GitError::InvalidFormat(
668                        "protocol v2 command argument is empty".into(),
669                    ));
670                }
671                if argument
672                    .iter()
673                    .any(|byte| matches!(*byte, b'\n' | b'\r' | 0))
674                {
675                    return Err(GitError::InvalidFormat(
676                        "protocol v2 command argument contains a delimiter byte".into(),
677                    ));
678                }
679                arguments.push(argument.to_vec());
680            }
681            PktLineFrame::Delimiter => {
682                if in_arguments {
683                    return Err(GitError::InvalidFormat(format!(
684                        "expected flush after {} arguments",
685                        command
686                    )));
687                }
688                if saw_flush {
689                    return Err(GitError::InvalidFormat(
690                        "protocol v2 command request has delimiter after flush".into(),
691                    ));
692                }
693                in_arguments = true;
694            }
695            PktLineFrame::Flush => {
696                saw_flush = true;
697                if idx + 1 != rest.len() {
698                    return Err(GitError::InvalidFormat(
699                        "protocol v2 command request has frames after flush".into(),
700                    ));
701                }
702            }
703            PktLineFrame::ResponseEnd => {
704                return Err(GitError::InvalidFormat(
705                    "protocol v2 command request contains response-end".into(),
706                ));
707            }
708        }
709    }
710    if !saw_flush {
711        return Err(GitError::InvalidFormat(
712            "protocol v2 command request missing flush".into(),
713        ));
714    }
715
716    Ok(ProtocolV2CommandRequest {
717        command,
718        capabilities,
719        arguments,
720    })
721}
722
723pub fn encode_protocol_v2_command_request(
724    request: &ProtocolV2CommandRequest,
725) -> Result<Vec<PktLineFrame>> {
726    validate_capability_name(&request.command)?;
727    let mut frames = Vec::new();
728    frames.push(PktLineFrame::data(line_from_str(&format!(
729        "command={}",
730        request.command
731    )))?);
732    for capability in &request.capabilities {
733        frames.push(PktLineFrame::data(line(encode_protocol_v2_capability(
734            capability,
735        )?))?);
736    }
737    if !request.arguments.is_empty() {
738        frames.push(PktLineFrame::Delimiter);
739        for argument in &request.arguments {
740            validate_protocol_v2_argument(argument)?;
741            let mut payload = argument.clone();
742            payload.push(b'\n');
743            frames.push(PktLineFrame::data(payload)?);
744        }
745    }
746    frames.push(PktLineFrame::Flush);
747    Ok(frames)
748}
749
750pub fn parse_protocol_v2_request(frames: &[PktLineFrame]) -> Result<ProtocolV2Request> {
751    if matches!(frames, [PktLineFrame::Flush]) {
752        return Ok(ProtocolV2Request::Done);
753    }
754    parse_protocol_v2_command_request(frames).map(ProtocolV2Request::Command)
755}
756
757pub fn encode_protocol_v2_request(request: &ProtocolV2Request) -> Result<Vec<PktLineFrame>> {
758    match request {
759        ProtocolV2Request::Command(command) => encode_protocol_v2_command_request(command),
760        ProtocolV2Request::Done => Ok(vec![PktLineFrame::Flush]),
761    }
762}
763
764pub fn read_protocol_v2_request(reader: &mut impl Read) -> Result<ProtocolV2Request> {
765    let frames = read_pkt_line_frames_until_flush(reader)?;
766    parse_protocol_v2_request(&frames)
767}
768
769pub fn write_protocol_v2_request(
770    writer: &mut impl Write,
771    request: &ProtocolV2Request,
772) -> Result<()> {
773    match request {
774        ProtocolV2Request::Command(command) => write_protocol_v2_command_request(writer, command),
775        ProtocolV2Request::Done => {
776            writer.write_all(b"0000")?;
777            Ok(())
778        }
779    }
780}
781
782pub fn read_protocol_v2_command_request(
783    reader: &mut impl Read,
784) -> Result<ProtocolV2CommandRequest> {
785    let mut frames = Vec::new();
786    loop {
787        let Some(frame) = read_pkt_line_frame(reader)? else {
788            if let Some(command) = frames.first().and_then(|frame| match frame {
789                PktLineFrame::Data(payload) => parse_protocol_v2_command_line(payload).ok(),
790                _ => None,
791            }) && frames
792                .iter()
793                .any(|frame| matches!(frame, PktLineFrame::Delimiter))
794            {
795                return Err(GitError::InvalidFormat(format!(
796                    "expected flush after {} arguments",
797                    command
798                )));
799            }
800            return Err(GitError::InvalidFormat(
801                "pkt-line stream ended before control packet".into(),
802            ));
803        };
804        let done = matches!(frame, PktLineFrame::Flush);
805        frames.push(frame);
806        if done {
807            break;
808        }
809    }
810    parse_protocol_v2_command_request(&frames)
811}
812
813pub fn write_protocol_v2_command_request(
814    writer: &mut impl Write,
815    request: &ProtocolV2CommandRequest,
816) -> Result<()> {
817    validate_capability_name(&request.command)?;
818    write_pkt_line_payload(
819        writer,
820        &line_from_str(&format!("command={}", request.command)),
821    )?;
822    for capability in &request.capabilities {
823        write_pkt_line_payload(writer, &line(encode_protocol_v2_capability(capability)?))?;
824    }
825    if !request.arguments.is_empty() {
826        write_pkt_line_frame(writer, &PktLineFrame::Delimiter)?;
827        for argument in &request.arguments {
828            validate_protocol_v2_argument(argument)?;
829            let mut payload = argument.clone();
830            payload.push(b'\n');
831            write_pkt_line_payload(writer, &payload)?;
832        }
833    }
834    writer.write_all(b"0000")?;
835    Ok(())
836}
837
838pub fn read_protocol_v2_ls_refs_request(reader: &mut impl Read) -> Result<ProtocolV2LsRefsRequest> {
839    let request = read_protocol_v2_command_request(reader)?;
840    ProtocolV2LsRefsRequest::from_command_request(&request)
841}
842
843pub fn write_protocol_v2_ls_refs_request(
844    writer: &mut impl Write,
845    request: &ProtocolV2LsRefsRequest,
846) -> Result<()> {
847    let command = request.to_command_request()?;
848    write_protocol_v2_command_request(writer, &command)
849}
850
851pub fn parse_protocol_v2_ls_refs_response(
852    format: ObjectFormat,
853    frames: &[PktLineFrame],
854) -> Result<Vec<ProtocolV2LsRefsRecord>> {
855    let mut records = Vec::new();
856    let mut saw_flush = false;
857    for (idx, frame) in frames.iter().enumerate() {
858        match frame {
859            PktLineFrame::Data(payload) => {
860                if saw_flush {
861                    return Err(GitError::InvalidFormat(
862                        "ls-refs response has data after flush".into(),
863                    ));
864                }
865                records.push(parse_protocol_v2_ls_refs_line(format, payload)?);
866            }
867            PktLineFrame::Flush => {
868                saw_flush = true;
869                if !flush_terminates_protocol_v2_response(frames, idx) {
870                    return Err(GitError::InvalidFormat(
871                        "ls-refs response has frames after flush".into(),
872                    ));
873                }
874            }
875            PktLineFrame::ResponseEnd if saw_flush && idx + 1 == frames.len() => {}
876            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
877                return Err(GitError::InvalidFormat(
878                    "ls-refs response contains a non-flush control packet".into(),
879                ));
880            }
881        }
882    }
883    if !saw_flush {
884        return Err(GitError::InvalidFormat(
885            "ls-refs response missing flush".into(),
886        ));
887    }
888    Ok(records)
889}
890
891pub fn encode_protocol_v2_ls_refs_response(
892    records: &[ProtocolV2LsRefsRecord],
893) -> Result<Vec<PktLineFrame>> {
894    let mut frames = Vec::new();
895    for record in records {
896        frames.push(PktLineFrame::data(line_from_str(
897            &format_protocol_v2_ls_refs_record(record)?,
898        ))?);
899    }
900    frames.push(PktLineFrame::Flush);
901    Ok(frames)
902}
903
904fn frames_start_with_protocol_v2_advertisement(frames: &[PktLineFrame]) -> bool {
905    matches!(
906        frames.first(),
907        Some(PktLineFrame::Data(payload)) if trim_trailing_lf(payload) == b"version 2"
908    )
909}
910
911/// Advance past a leading protocol v2 capability advertisement when present.
912/// Returns the first non-advertisement frame when the stream does not begin with
913/// `version 2`.
914///
915/// A capability advertisement (`version 2` … flush) is never sideband-wrapped: it
916/// is emitted before the fetch command's response body, and sideband multiplexing
917/// only applies to the fetch response itself. The advertisement's leading pkt is a
918/// raw `version 2`, whose first byte (`v`, 0x76) can never collide with a sideband
919/// channel byte (0x01–0x03), so the advertisement check is unambiguous even under
920/// `sideband-all`.
921///
922/// When `sideband_all` is negotiated and the stream does *not* begin with an
923/// advertisement, the first fetch-response frame (a section header such as
924/// `acknowledgments`, or a leading channel-2 progress frame) arrives
925/// sideband-wrapped. We demux it here so the section-header reader in
926/// `read_protocol_v2_fetch_response_header` sees a plain payload rather than a raw
927/// control byte.
928fn skip_leading_protocol_v2_advertisement_if_present(
929    reader: &mut impl Read,
930    sideband_all: bool,
931) -> Result<Option<PktLineFrame>> {
932    let first = read_pkt_line_frame(reader)?.ok_or_else(|| {
933        GitError::InvalidFormat("protocol v2 response ended before first pkt-line".into())
934    })?;
935    let PktLineFrame::Data(payload) = &first else {
936        return Ok(Some(first));
937    };
938    if trim_trailing_lf(payload) != b"version 2" {
939        if sideband_all {
940            // Not an advertisement: the first fetch-response frame is
941            // sideband-wrapped. Demux it, skipping a leading progress frame,
942            // so the caller receives the demultiplexed payload.
943            let packet = parse_sideband_packet(payload)?;
944            let demuxed = match packet.channel {
945                SideBandChannel::Data => PktLineFrame::Data(packet.data),
946                SideBandChannel::Progress => read_protocol_v2_fetch_metadata_frame(reader, true)?,
947                SideBandChannel::Fatal => {
948                    let message = String::from_utf8_lossy(&packet.data).into_owned();
949                    return Err(GitError::InvalidFormat(format!(
950                        "sideband fatal: {message}"
951                    )));
952                }
953            };
954            return Ok(Some(demuxed));
955        }
956        return Ok(Some(first));
957    }
958    loop {
959        match read_pkt_line_frame(reader)? {
960            Some(PktLineFrame::Flush) => return Ok(None),
961            Some(PktLineFrame::Data(_)) => {}
962            Some(_) => {
963                return Err(GitError::InvalidFormat(
964                    "protocol v2 capability advertisement contains a non-flush control packet"
965                        .into(),
966                ));
967            }
968            None => {
969                return Err(GitError::InvalidFormat(
970                    "protocol v2 capability advertisement missing flush".into(),
971                ));
972            }
973        }
974    }
975}
976
977/// Read the payload section of a stateless smart-HTTP v2 RPC response, skipping a
978/// leading capability advertisement when the server includes one before the
979/// command result.
980pub fn read_protocol_v2_stateless_rpc_payload_frames(
981    reader: &mut impl Read,
982) -> Result<Vec<PktLineFrame>> {
983    let mut frames = read_pkt_line_frames_until_flush(reader)?;
984    if frames_start_with_protocol_v2_advertisement(&frames) {
985        frames = read_pkt_line_frames_until_flush(reader)?;
986    }
987    Ok(frames)
988}
989
990pub fn read_protocol_v2_ls_refs_response(
991    format: ObjectFormat,
992    reader: &mut impl Read,
993) -> Result<Vec<ProtocolV2LsRefsRecord>> {
994    let frames = read_protocol_v2_stateless_rpc_payload_frames(reader)?;
995    parse_protocol_v2_ls_refs_response(format, &frames)
996}
997
998pub fn write_protocol_v2_ls_refs_response(
999    writer: &mut impl Write,
1000    records: &[ProtocolV2LsRefsRecord],
1001) -> Result<()> {
1002    for record in records {
1003        write_pkt_line_payload(
1004            writer,
1005            &line_from_str(&format_protocol_v2_ls_refs_record(record)?),
1006        )?;
1007    }
1008    writer.write_all(b"0000")?;
1009    Ok(())
1010}
1011
1012pub fn read_protocol_v2_ls_refs_response_until_response_end(
1013    format: ObjectFormat,
1014    reader: &mut impl Read,
1015) -> Result<Vec<ProtocolV2LsRefsRecord>> {
1016    let frames = read_pkt_line_frames_until_response_end(reader)?;
1017    parse_protocol_v2_ls_refs_response(format, &frames)
1018}
1019
1020pub fn write_protocol_v2_ls_refs_response_with_response_end(
1021    writer: &mut impl Write,
1022    records: &[ProtocolV2LsRefsRecord],
1023) -> Result<()> {
1024    write_protocol_v2_ls_refs_response(writer, records)?;
1025    writer.write_all(b"0002")?;
1026    Ok(())
1027}
1028
1029pub fn exchange_protocol_v2_ls_refs(
1030    format: ObjectFormat,
1031    reader: &mut impl Read,
1032    writer: &mut impl Write,
1033    request: &ProtocolV2LsRefsRequest,
1034) -> Result<Vec<ProtocolV2LsRefsRecord>> {
1035    write_protocol_v2_ls_refs_request(writer, request)?;
1036    writer.flush()?;
1037    read_protocol_v2_ls_refs_response(format, reader)
1038}
1039
1040/// Bridge a parsed protocol v2 `ls-refs` response into the shared
1041/// [`RefAdvertisementSet`]/[`RefAdvertisement`] types used by the v0/v1 codecs,
1042/// so callers can drive v2 clone/fetch through the same ref-advertisement
1043/// machinery.
1044///
1045/// Each [`ProtocolV2LsRefsRecord::Ref`] becomes a [`RefAdvertisement`]. A
1046/// `peeled:<oid>` attribute is emitted as an additional `<peeled-oid>
1047/// <name>^{}` advertisement, matching the v0/v1 peeled-tag convention.
1048/// `symref-target:<target>` attributes are collected as `symref=<name>:<target>`
1049/// capabilities on the first advertised ref, mirroring how the upload-pack v0/v1
1050/// advertisement carries symrefs. [`ProtocolV2LsRefsRecord::Unborn`] records have
1051/// no object id, so they cannot be represented as a [`RefAdvertisement`]; an
1052/// unborn record carrying a `symref-target` is preserved as a `symref` capability
1053/// while otherwise being skipped. The returned set always reports
1054/// [`ProtocolVersion::V2`].
1055pub fn protocol_v2_ls_refs_records_to_ref_advertisement_set(
1056    records: &[ProtocolV2LsRefsRecord],
1057) -> Result<RefAdvertisementSet> {
1058    let mut refs: Vec<RefAdvertisement> = Vec::new();
1059    let mut symrefs: Vec<Capability> = Vec::new();
1060    for record in records {
1061        match record {
1062            ProtocolV2LsRefsRecord::Ref(reference) => {
1063                validate_protocol_v2_token("ls-refs ref name", &reference.name)?;
1064                refs.push(RefAdvertisement {
1065                    oid: reference.oid,
1066                    name: reference.name.clone(),
1067                    capabilities: Vec::new(),
1068                });
1069                if let Some(peeled) = &reference.peeled {
1070                    refs.push(RefAdvertisement {
1071                        oid: peeled.clone(),
1072                        name: format!("{}^{{}}", reference.name),
1073                        capabilities: Vec::new(),
1074                    });
1075                }
1076                if let Some(target) = &reference.symref_target {
1077                    symrefs.push(protocol_v2_symref_capability(&reference.name, target)?);
1078                }
1079            }
1080            ProtocolV2LsRefsRecord::Unborn {
1081                name,
1082                symref_target,
1083                ..
1084            } => {
1085                validate_protocol_v2_token("ls-refs ref name", name)?;
1086                if let Some(target) = symref_target {
1087                    symrefs.push(protocol_v2_symref_capability(name, target)?);
1088                }
1089            }
1090        }
1091    }
1092    if !symrefs.is_empty() {
1093        if let Some(first) = refs.first_mut() {
1094            first.capabilities = symrefs;
1095        } else {
1096            return Err(GitError::InvalidFormat(
1097                "ls-refs response advertised symrefs without any concrete refs".into(),
1098            ));
1099        }
1100    }
1101    Ok(RefAdvertisementSet {
1102        protocol: ProtocolVersion::V2,
1103        refs,
1104        shallow: Vec::new(),
1105    })
1106}
1107
1108/// Parse a protocol v2 `ls-refs` response and bridge it into the shared
1109/// [`RefAdvertisementSet`] type. Convenience wrapper combining
1110/// [`parse_protocol_v2_ls_refs_response`] and
1111/// [`protocol_v2_ls_refs_records_to_ref_advertisement_set`].
1112pub fn parse_protocol_v2_ls_refs_response_as_ref_advertisement_set(
1113    format: ObjectFormat,
1114    frames: &[PktLineFrame],
1115) -> Result<RefAdvertisementSet> {
1116    let records = parse_protocol_v2_ls_refs_response(format, frames)?;
1117    protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
1118}
1119
1120/// Read a protocol v2 `ls-refs` response from `reader` and bridge it into the
1121/// shared [`RefAdvertisementSet`] type.
1122pub fn read_protocol_v2_ls_refs_response_as_ref_advertisement_set(
1123    format: ObjectFormat,
1124    reader: &mut impl Read,
1125) -> Result<RefAdvertisementSet> {
1126    let records = read_protocol_v2_ls_refs_response(format, reader)?;
1127    protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
1128}
1129
1130fn protocol_v2_symref_capability(name: &str, target: &str) -> Result<Capability> {
1131    validate_protocol_v2_token("ls-refs symref-target", target)?;
1132    Ok(Capability {
1133        name: "symref".into(),
1134        value: Some(format!("{name}:{target}")),
1135    })
1136}
1137
1138pub fn read_protocol_v2_fetch_request(
1139    format: ObjectFormat,
1140    reader: &mut impl Read,
1141) -> Result<ProtocolV2FetchRequest> {
1142    let request = read_protocol_v2_command_request(reader)?;
1143    ProtocolV2FetchRequest::from_command_request(format, &request)
1144}
1145
1146pub fn write_protocol_v2_fetch_request(
1147    writer: &mut impl Write,
1148    request: &ProtocolV2FetchRequest,
1149) -> Result<()> {
1150    let command = request.to_command_request()?;
1151    write_protocol_v2_command_request(writer, &command)
1152}
1153
1154pub fn read_protocol_v2_object_info_request(
1155    format: ObjectFormat,
1156    reader: &mut impl Read,
1157) -> Result<ProtocolV2ObjectInfoRequest> {
1158    let request = read_protocol_v2_command_request(reader)?;
1159    ProtocolV2ObjectInfoRequest::from_command_request(format, &request)
1160}
1161
1162pub fn write_protocol_v2_object_info_request(
1163    writer: &mut impl Write,
1164    request: &ProtocolV2ObjectInfoRequest,
1165) -> Result<()> {
1166    let command = request.to_command_request()?;
1167    write_protocol_v2_command_request(writer, &command)
1168}
1169
1170pub fn parse_protocol_v2_fetch_response(
1171    format: ObjectFormat,
1172    frames: &[PktLineFrame],
1173) -> Result<Vec<ProtocolV2FetchResponseSection>> {
1174    let mut sections = Vec::new();
1175    let mut current: Option<(String, Vec<Vec<u8>>)> = None;
1176    let mut saw_flush = false;
1177    for (idx, frame) in frames.iter().enumerate() {
1178        match frame {
1179            PktLineFrame::Data(payload) => {
1180                if saw_flush {
1181                    return Err(GitError::InvalidFormat(
1182                        "fetch response has data after flush".into(),
1183                    ));
1184                }
1185                if let Some((_name, lines)) = &mut current {
1186                    lines.push(payload.clone());
1187                } else {
1188                    let name = parse_fetch_section_header(payload)?;
1189                    current = Some((name, Vec::new()));
1190                }
1191            }
1192            PktLineFrame::Delimiter => {
1193                if saw_flush {
1194                    return Err(GitError::InvalidFormat(
1195                        "fetch response has delimiter after flush".into(),
1196                    ));
1197                }
1198                let Some((name, lines)) = current.take() else {
1199                    return Err(GitError::InvalidFormat(
1200                        "fetch response has delimiter before section".into(),
1201                    ));
1202                };
1203                sections.push(parse_fetch_section(format, name, lines)?);
1204            }
1205            PktLineFrame::Flush => {
1206                saw_flush = true;
1207                if !flush_terminates_protocol_v2_response(frames, idx) {
1208                    return Err(GitError::InvalidFormat(
1209                        "fetch response has frames after flush".into(),
1210                    ));
1211                }
1212                if let Some((name, lines)) = current.take() {
1213                    sections.push(parse_fetch_section(format, name, lines)?);
1214                }
1215            }
1216            PktLineFrame::ResponseEnd if saw_flush && idx + 1 == frames.len() => {}
1217            PktLineFrame::ResponseEnd => {
1218                return Err(GitError::InvalidFormat(
1219                    "fetch response contains response-end".into(),
1220                ));
1221            }
1222        }
1223    }
1224    if !saw_flush {
1225        return Err(GitError::InvalidFormat(
1226            "fetch response missing flush".into(),
1227        ));
1228    }
1229    Ok(sections)
1230}
1231
1232pub fn encode_protocol_v2_fetch_response(
1233    sections: &[ProtocolV2FetchResponseSection],
1234) -> Result<Vec<PktLineFrame>> {
1235    let mut frames = Vec::new();
1236    for (idx, section) in sections.iter().enumerate() {
1237        if idx != 0 {
1238            frames.push(PktLineFrame::Delimiter);
1239        }
1240        frames.push(PktLineFrame::data(line_from_str(
1241            protocol_v2_fetch_section_name(section),
1242        ))?);
1243        for line in format_protocol_v2_fetch_section_lines(section)? {
1244            frames.push(PktLineFrame::data(line)?);
1245        }
1246    }
1247    frames.push(PktLineFrame::Flush);
1248    Ok(frames)
1249}
1250
1251pub fn parse_protocol_v2_fetch_sideband_all_response(
1252    format: ObjectFormat,
1253    frames: &[PktLineFrame],
1254) -> Result<ProtocolV2FetchSidebandAllResponse> {
1255    let mut demuxed = Vec::new();
1256    let mut progress = Vec::new();
1257    let mut in_packfile = false;
1258    for frame in frames {
1259        match frame {
1260            PktLineFrame::Data(payload) if in_packfile => {
1261                demuxed.push(PktLineFrame::Data(payload.clone()));
1262            }
1263            PktLineFrame::Data(payload) => {
1264                let packet = parse_sideband_packet(payload)?;
1265                match packet.channel {
1266                    SideBandChannel::Data => {
1267                        if trim_trailing_lf(&packet.data) == b"packfile" {
1268                            in_packfile = true;
1269                        }
1270                        demuxed.push(PktLineFrame::Data(packet.data));
1271                    }
1272                    SideBandChannel::Progress => progress.push(packet.data),
1273                    SideBandChannel::Fatal => {
1274                        let message = String::from_utf8_lossy(&packet.data).into_owned();
1275                        return Err(GitError::InvalidFormat(format!(
1276                            "sideband fatal: {message}"
1277                        )));
1278                    }
1279                }
1280            }
1281            PktLineFrame::Delimiter => {
1282                in_packfile = false;
1283                demuxed.push(PktLineFrame::Delimiter);
1284            }
1285            PktLineFrame::Flush => {
1286                in_packfile = false;
1287                demuxed.push(PktLineFrame::Flush);
1288            }
1289            PktLineFrame::ResponseEnd => {
1290                in_packfile = false;
1291                demuxed.push(PktLineFrame::ResponseEnd);
1292            }
1293        }
1294    }
1295    Ok(ProtocolV2FetchSidebandAllResponse {
1296        sections: parse_protocol_v2_fetch_response(format, &demuxed)?,
1297        progress,
1298    })
1299}
1300
1301pub fn encode_protocol_v2_fetch_sideband_all_response(
1302    sections: &[ProtocolV2FetchResponseSection],
1303) -> Result<Vec<PktLineFrame>> {
1304    let frames = encode_protocol_v2_fetch_response(sections)?;
1305    let mut encoded = Vec::new();
1306    let mut in_packfile = false;
1307    for frame in frames {
1308        match frame {
1309            PktLineFrame::Data(payload) if in_packfile => {
1310                encoded.push(PktLineFrame::Data(payload));
1311            }
1312            PktLineFrame::Data(payload) => {
1313                if trim_trailing_lf(&payload) == b"packfile" {
1314                    in_packfile = true;
1315                }
1316                encoded.push(PktLineFrame::data(encode_sideband_packet(
1317                    &SideBandPacket {
1318                        channel: SideBandChannel::Data,
1319                        data: payload,
1320                    },
1321                )?)?);
1322            }
1323            PktLineFrame::Delimiter => {
1324                in_packfile = false;
1325                encoded.push(PktLineFrame::Delimiter);
1326            }
1327            PktLineFrame::Flush => {
1328                in_packfile = false;
1329                encoded.push(PktLineFrame::Flush);
1330            }
1331            PktLineFrame::ResponseEnd => {
1332                in_packfile = false;
1333                encoded.push(PktLineFrame::ResponseEnd);
1334            }
1335        }
1336    }
1337    Ok(encoded)
1338}
1339
1340pub fn read_protocol_v2_fetch_response(
1341    format: ObjectFormat,
1342    reader: &mut impl Read,
1343) -> Result<Vec<ProtocolV2FetchResponseSection>> {
1344    let frames = read_protocol_v2_stateless_rpc_payload_frames(reader)?;
1345    parse_protocol_v2_fetch_response(format, &frames)
1346}
1347
1348pub fn read_protocol_v2_fetch_response_header(
1349    format: ObjectFormat,
1350    reader: &mut impl Read,
1351    sideband_all: bool,
1352) -> Result<ProtocolV2FetchResponseHeader> {
1353    let mut pending = skip_leading_protocol_v2_advertisement_if_present(reader, sideband_all)?;
1354    let mut sections = Vec::new();
1355    let mut current: Option<(String, Vec<Vec<u8>>)> = None;
1356    loop {
1357        let frame = if let Some(frame) = pending.take() {
1358            frame
1359        } else {
1360            read_protocol_v2_fetch_metadata_frame(reader, sideband_all)?
1361        };
1362        match frame {
1363            PktLineFrame::Data(payload) => {
1364                if let Some((_name, lines)) = &mut current {
1365                    lines.push(payload);
1366                } else {
1367                    let name = parse_fetch_section_header(&payload)?;
1368                    if name == "packfile" {
1369                        return Ok(ProtocolV2FetchResponseHeader {
1370                            sections,
1371                            has_packfile: true,
1372                        });
1373                    }
1374                    current = Some((name, Vec::new()));
1375                }
1376            }
1377            PktLineFrame::Delimiter => {
1378                let Some((name, lines)) = current.take() else {
1379                    return Err(GitError::InvalidFormat(
1380                        "fetch response has delimiter before section".into(),
1381                    ));
1382                };
1383                sections.push(parse_fetch_section(format, name, lines)?);
1384            }
1385            PktLineFrame::Flush => {
1386                if let Some((name, lines)) = current.take() {
1387                    sections.push(parse_fetch_section(format, name, lines)?);
1388                }
1389                return Ok(ProtocolV2FetchResponseHeader {
1390                    sections,
1391                    has_packfile: false,
1392                });
1393            }
1394            PktLineFrame::ResponseEnd => {
1395                return Err(GitError::InvalidFormat(
1396                    "fetch response contains response-end".into(),
1397                ));
1398            }
1399        }
1400    }
1401}
1402
1403/// Read and validate the acknowledgment-only prefix of a multi-round fetch.
1404///
1405/// A server that says `ready` must delimit the acknowledgment section before
1406/// sending the pack, except when the client requested `wait-for-done`. A server
1407/// that has not said `ready` must flush the response so the client can continue
1408/// negotiation. Keeping this request-dependent validation here preserves the
1409/// generic response parser's support for valid `ready` + flush intermediate
1410/// responses.
1411pub fn read_protocol_v2_fetch_negotiation_response(
1412    format: ObjectFormat,
1413    reader: &mut impl Read,
1414    sideband_all: bool,
1415    wait_for_done: bool,
1416) -> Result<ProtocolV2FetchNegotiationResponse> {
1417    let mut pending = skip_leading_protocol_v2_advertisement_if_present(reader, sideband_all)?;
1418    let first = if let Some(frame) = pending.take() {
1419        frame
1420    } else {
1421        read_protocol_v2_fetch_metadata_frame(reader, sideband_all)?
1422    };
1423    let PktLineFrame::Data(header) = first else {
1424        return Err(GitError::InvalidFormat(
1425            "fetch negotiation response is missing acknowledgments".into(),
1426        ));
1427    };
1428    if parse_fetch_section_header(&header)? != "acknowledgments" {
1429        return Err(GitError::InvalidFormat(
1430            "fetch negotiation response must begin with acknowledgments".into(),
1431        ));
1432    }
1433
1434    let mut acknowledgments = Vec::new();
1435    loop {
1436        match read_protocol_v2_fetch_metadata_frame(reader, sideband_all)? {
1437            PktLineFrame::Data(line) => {
1438                acknowledgments.push(parse_fetch_acknowledgment(format, &line)?);
1439            }
1440            PktLineFrame::Delimiter => {
1441                let ready = acknowledgments
1442                    .iter()
1443                    .any(|ack| matches!(ack, ProtocolV2FetchAcknowledgment::Ready));
1444                if !ready {
1445                    return Err(GitError::InvalidFormat(
1446                        "expected no other sections to be sent after no 'ready'".into(),
1447                    ));
1448                }
1449                return Ok(ProtocolV2FetchNegotiationResponse {
1450                    acknowledgments,
1451                    has_following_sections: true,
1452                });
1453            }
1454            PktLineFrame::Flush => {
1455                let ready = acknowledgments
1456                    .iter()
1457                    .any(|ack| matches!(ack, ProtocolV2FetchAcknowledgment::Ready));
1458                if ready && !wait_for_done {
1459                    return Err(GitError::InvalidFormat(
1460                        "expected packfile to be sent after 'ready'".into(),
1461                    ));
1462                }
1463                return Ok(ProtocolV2FetchNegotiationResponse {
1464                    acknowledgments,
1465                    has_following_sections: false,
1466                });
1467            }
1468            PktLineFrame::ResponseEnd => {
1469                return Err(GitError::InvalidFormat(
1470                    "fetch negotiation response contains response-end".into(),
1471                ));
1472            }
1473        }
1474    }
1475}
1476
1477fn read_protocol_v2_fetch_metadata_frame(
1478    reader: &mut impl Read,
1479    sideband_all: bool,
1480) -> Result<PktLineFrame> {
1481    loop {
1482        let frame = read_pkt_line_frame(reader)?
1483            .ok_or_else(|| GitError::InvalidFormat("fetch response ended before flush".into()))?;
1484        if sideband_all && let PktLineFrame::Data(payload) = frame {
1485            let packet = parse_sideband_packet(&payload)?;
1486            match packet.channel {
1487                SideBandChannel::Data => return Ok(PktLineFrame::Data(packet.data)),
1488                SideBandChannel::Progress => continue,
1489                SideBandChannel::Fatal => {
1490                    let message = String::from_utf8_lossy(&packet.data).into_owned();
1491                    return Err(GitError::InvalidFormat(format!(
1492                        "sideband fatal: {message}"
1493                    )));
1494                }
1495            }
1496        }
1497        return Ok(frame);
1498    }
1499}
1500
1501pub fn write_protocol_v2_fetch_response(
1502    writer: &mut impl Write,
1503    sections: &[ProtocolV2FetchResponseSection],
1504) -> Result<()> {
1505    write_protocol_v2_fetch_response_inner(writer, sections, false, false)
1506}
1507
1508pub fn read_protocol_v2_fetch_sideband_all_response(
1509    format: ObjectFormat,
1510    reader: &mut impl Read,
1511) -> Result<ProtocolV2FetchSidebandAllResponse> {
1512    let frames = read_protocol_v2_stateless_rpc_payload_frames(reader)?;
1513    parse_protocol_v2_fetch_sideband_all_response(format, &frames)
1514}
1515
1516pub fn write_protocol_v2_fetch_sideband_all_response(
1517    writer: &mut impl Write,
1518    sections: &[ProtocolV2FetchResponseSection],
1519) -> Result<()> {
1520    write_protocol_v2_fetch_response_inner(writer, sections, true, false)
1521}
1522
1523pub fn read_protocol_v2_fetch_response_until_response_end(
1524    format: ObjectFormat,
1525    reader: &mut impl Read,
1526) -> Result<Vec<ProtocolV2FetchResponseSection>> {
1527    let frames = read_pkt_line_frames_until_response_end(reader)?;
1528    parse_protocol_v2_fetch_response(format, &frames)
1529}
1530
1531pub fn write_protocol_v2_fetch_response_with_response_end(
1532    writer: &mut impl Write,
1533    sections: &[ProtocolV2FetchResponseSection],
1534) -> Result<()> {
1535    write_protocol_v2_fetch_response_inner(writer, sections, false, true)
1536}
1537
1538pub fn read_protocol_v2_fetch_sideband_all_response_until_response_end(
1539    format: ObjectFormat,
1540    reader: &mut impl Read,
1541) -> Result<ProtocolV2FetchSidebandAllResponse> {
1542    let frames = read_pkt_line_frames_until_response_end(reader)?;
1543    parse_protocol_v2_fetch_sideband_all_response(format, &frames)
1544}
1545
1546pub fn write_protocol_v2_fetch_sideband_all_response_with_response_end(
1547    writer: &mut impl Write,
1548    sections: &[ProtocolV2FetchResponseSection],
1549) -> Result<()> {
1550    write_protocol_v2_fetch_response_inner(writer, sections, true, true)
1551}
1552
1553fn write_protocol_v2_fetch_response_inner(
1554    writer: &mut impl Write,
1555    sections: &[ProtocolV2FetchResponseSection],
1556    sideband_all: bool,
1557    response_end: bool,
1558) -> Result<()> {
1559    let mut in_packfile = false;
1560    for (idx, section) in sections.iter().enumerate() {
1561        if idx != 0 {
1562            in_packfile = false;
1563            write_pkt_line_frame(writer, &PktLineFrame::Delimiter)?;
1564        }
1565        write_protocol_v2_fetch_payload(
1566            writer,
1567            &line_from_str(protocol_v2_fetch_section_name(section)),
1568            sideband_all,
1569            &mut in_packfile,
1570        )?;
1571        for payload in format_protocol_v2_fetch_section_lines(section)? {
1572            write_protocol_v2_fetch_payload(writer, &payload, sideband_all, &mut in_packfile)?;
1573        }
1574    }
1575    writer.write_all(b"0000")?;
1576    if response_end {
1577        writer.write_all(b"0002")?;
1578    }
1579    Ok(())
1580}
1581
1582fn write_protocol_v2_fetch_payload(
1583    writer: &mut impl Write,
1584    payload: &[u8],
1585    sideband_all: bool,
1586    in_packfile: &mut bool,
1587) -> Result<()> {
1588    if sideband_all && !*in_packfile {
1589        if trim_trailing_lf(payload) == b"packfile" {
1590            *in_packfile = true;
1591        }
1592        write_sideband_payload(writer, SideBandChannel::Data, payload)
1593    } else {
1594        write_pkt_line_payload(writer, payload)
1595    }
1596}
1597
1598pub fn exchange_protocol_v2_fetch(
1599    format: ObjectFormat,
1600    reader: &mut impl Read,
1601    writer: &mut impl Write,
1602    request: &ProtocolV2FetchRequest,
1603) -> Result<Vec<ProtocolV2FetchResponseSection>> {
1604    write_protocol_v2_fetch_request(writer, request)?;
1605    writer.flush()?;
1606    read_protocol_v2_fetch_response(format, reader)
1607}
1608
1609pub fn parse_protocol_v2_object_info_response(
1610    format: ObjectFormat,
1611    frames: &[PktLineFrame],
1612) -> Result<ProtocolV2ObjectInfoResponse> {
1613    let Some((first, rest)) = frames.split_first() else {
1614        return Err(GitError::InvalidFormat(
1615            "object-info response is empty".into(),
1616        ));
1617    };
1618    let PktLineFrame::Data(attrs) = first else {
1619        return Err(GitError::InvalidFormat(
1620            "object-info response must start with attributes".into(),
1621        ));
1622    };
1623    let attrs = parse_protocol_v2_line_text("object-info response attributes", attrs)?;
1624    let mut response = ProtocolV2ObjectInfoResponse::default();
1625    for attr in attrs.split(' ') {
1626        validate_protocol_v2_token("object-info response attribute", attr)?;
1627        match attr {
1628            "size" => {
1629                if response.size {
1630                    return Err(GitError::InvalidFormat(
1631                        "object-info response has duplicate size attribute".into(),
1632                    ));
1633                }
1634                response.size = true;
1635            }
1636            other => {
1637                return Err(GitError::InvalidFormat(format!(
1638                    "unsupported object-info response attribute {other}"
1639                )));
1640            }
1641        }
1642    }
1643    if !response.size {
1644        return Err(GitError::InvalidFormat(
1645            "object-info response is missing size attribute".into(),
1646        ));
1647    }
1648
1649    let mut saw_flush = false;
1650    for (idx, frame) in rest.iter().enumerate() {
1651        match frame {
1652            PktLineFrame::Data(payload) if !saw_flush => {
1653                response
1654                    .records
1655                    .push(parse_protocol_v2_object_info_record(format, payload)?);
1656            }
1657            PktLineFrame::Data(_) => {
1658                return Err(GitError::InvalidFormat(
1659                    "object-info response has data after flush".into(),
1660                ));
1661            }
1662            PktLineFrame::Flush => {
1663                saw_flush = true;
1664                if idx + 1 != rest.len() {
1665                    return Err(GitError::InvalidFormat(
1666                        "object-info response has frames after flush".into(),
1667                    ));
1668                }
1669            }
1670            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
1671                return Err(GitError::InvalidFormat(
1672                    "object-info response contains a non-flush control packet".into(),
1673                ));
1674            }
1675        }
1676    }
1677    if !saw_flush {
1678        return Err(GitError::InvalidFormat(
1679            "object-info response missing flush".into(),
1680        ));
1681    }
1682    Ok(response)
1683}
1684
1685pub fn encode_protocol_v2_object_info_response(
1686    response: &ProtocolV2ObjectInfoResponse,
1687) -> Result<Vec<PktLineFrame>> {
1688    if !response.size {
1689        return Err(GitError::InvalidFormat(
1690            "object-info response is missing size attribute".into(),
1691        ));
1692    }
1693    let mut frames = Vec::new();
1694    frames.push(PktLineFrame::data(line_from_str("size"))?);
1695    for record in &response.records {
1696        frames.push(PktLineFrame::data(line_from_str(&format!(
1697            "{} {}",
1698            record.oid, record.size
1699        )))?);
1700    }
1701    frames.push(PktLineFrame::Flush);
1702    Ok(frames)
1703}
1704
1705pub fn read_protocol_v2_object_info_response(
1706    format: ObjectFormat,
1707    reader: &mut impl Read,
1708) -> Result<ProtocolV2ObjectInfoResponse> {
1709    let frames = read_pkt_line_frames_until_flush(reader)?;
1710    parse_protocol_v2_object_info_response(format, &frames)
1711}
1712
1713pub fn write_protocol_v2_object_info_response(
1714    writer: &mut impl Write,
1715    response: &ProtocolV2ObjectInfoResponse,
1716) -> Result<()> {
1717    if !response.size {
1718        return Err(GitError::InvalidFormat(
1719            "object-info response is missing size attribute".into(),
1720        ));
1721    }
1722    write_pkt_line_payload(writer, b"size\n")?;
1723    for record in &response.records {
1724        write_pkt_line_payload(
1725            writer,
1726            &line_from_str(&format!("{} {}", record.oid, record.size)),
1727        )?;
1728    }
1729    writer.write_all(b"0000")?;
1730    Ok(())
1731}
1732
1733pub fn exchange_protocol_v2_object_info(
1734    format: ObjectFormat,
1735    reader: &mut impl Read,
1736    writer: &mut impl Write,
1737    request: &ProtocolV2ObjectInfoRequest,
1738) -> Result<ProtocolV2ObjectInfoResponse> {
1739    write_protocol_v2_object_info_request(writer, request)?;
1740    writer.flush()?;
1741    read_protocol_v2_object_info_response(format, reader)
1742}
1743
1744pub fn demux_protocol_v2_fetch_packfile(
1745    sections: &[ProtocolV2FetchResponseSection],
1746) -> Result<Option<SideBandDemux>> {
1747    let mut packfile = None;
1748    for section in sections {
1749        if let ProtocolV2FetchResponseSection::Packfile(lines) = section {
1750            if packfile.is_some() {
1751                return Err(GitError::InvalidFormat(
1752                    "fetch response has duplicate packfile sections".into(),
1753                ));
1754            }
1755            packfile = Some(parse_and_demux_sideband_packets(lines)?);
1756        }
1757    }
1758    Ok(packfile)
1759}
1760
1761pub fn protocol_v2_object_format(capabilities: &[Capability]) -> Result<ObjectFormat> {
1762    let mut format = None;
1763    for capability in capabilities {
1764        if capability.name != "object-format" {
1765            continue;
1766        }
1767        if format.is_some() {
1768            return Err(GitError::InvalidFormat(
1769                "protocol v2 has duplicate object-format capabilities".into(),
1770            ));
1771        }
1772        let Some(value) = &capability.value else {
1773            return Err(GitError::InvalidFormat(
1774                "protocol v2 object-format capability is missing a value".into(),
1775            ));
1776        };
1777        format = Some(value.parse::<ObjectFormat>()?);
1778    }
1779    Ok(format.unwrap_or(ObjectFormat::Sha1))
1780}
1781
1782pub fn validate_protocol_v2_command_request_capabilities(
1783    handshake: &TransportHandshake,
1784    request: &ProtocolV2CommandRequest,
1785) -> Result<()> {
1786    if handshake.protocol != ProtocolVersion::V2 {
1787        return Err(GitError::InvalidFormat(
1788            "protocol v2 command validation requires a v2 handshake".into(),
1789        ));
1790    }
1791    let advertised =
1792        protocol_v2_capability(&handshake.capabilities, &request.command).ok_or_else(|| {
1793            GitError::InvalidFormat(format!("unadvertised command {}", request.command))
1794        })?;
1795    if advertised.name.is_empty() {
1796        return Err(GitError::InvalidFormat(
1797            "advertised command capability is empty".into(),
1798        ));
1799    }
1800    parse_protocol_v2_command_options(&request.capabilities)?;
1801
1802    for capability in &request.capabilities {
1803        let advertised = protocol_v2_capability(&handshake.capabilities, &capability.name)
1804            .ok_or_else(|| {
1805                GitError::InvalidFormat(format!(
1806                    "unadvertised protocol v2 capability {}",
1807                    capability.name
1808                ))
1809            })?;
1810        if capability.name == "object-format" {
1811            validate_protocol_v2_object_format_request(advertised, capability)?;
1812        }
1813    }
1814    Ok(())
1815}
1816
1817pub fn parse_protocol_v2_command_options(
1818    capabilities: &[Capability],
1819) -> Result<ProtocolV2CommandOptions> {
1820    let mut out = ProtocolV2CommandOptions::default();
1821    for capability in capabilities {
1822        match capability.name.as_str() {
1823            "agent" => {
1824                if out.agent.is_some() {
1825                    return Err(GitError::InvalidFormat(
1826                        "protocol v2 command has duplicate agent capabilities".into(),
1827                    ));
1828                }
1829                let Some(value) = &capability.value else {
1830                    return Err(GitError::InvalidFormat(
1831                        "protocol v2 agent capability is missing a value".into(),
1832                    ));
1833                };
1834                validate_protocol_v2_capability_value(value)?;
1835                out.agent = Some(value.clone());
1836            }
1837            "object-format" => {
1838                if out.object_format.is_some() {
1839                    return Err(GitError::InvalidFormat(
1840                        "protocol v2 command has duplicate object-format capabilities".into(),
1841                    ));
1842                }
1843                let Some(value) = &capability.value else {
1844                    return Err(GitError::InvalidFormat(
1845                        "protocol v2 object-format capability is missing a value".into(),
1846                    ));
1847                };
1848                out.object_format = Some(value.parse::<ObjectFormat>()?);
1849            }
1850            "server-option" => {
1851                let Some(value) = &capability.value else {
1852                    return Err(GitError::InvalidFormat(
1853                        "protocol v2 server-option capability is missing a value".into(),
1854                    ));
1855                };
1856                validate_protocol_v2_capability_value(value)?;
1857                out.server_options.push(value.clone());
1858            }
1859            _ => out.extra.push(capability.clone()),
1860        }
1861    }
1862    Ok(out)
1863}
1864
1865pub fn encode_protocol_v2_command_options(
1866    options: &ProtocolV2CommandOptions,
1867) -> Result<Vec<Capability>> {
1868    let mut capabilities = Vec::new();
1869    if let Some(agent) = &options.agent {
1870        validate_protocol_v2_capability_value(agent)?;
1871        capabilities.push(Capability {
1872            name: "agent".into(),
1873            value: Some(agent.clone()),
1874        });
1875    }
1876    if let Some(format) = options.object_format {
1877        capabilities.push(Capability {
1878            name: "object-format".into(),
1879            value: Some(format.name().into()),
1880        });
1881    }
1882    for option in &options.server_options {
1883        validate_protocol_v2_capability_value(option)?;
1884        capabilities.push(Capability {
1885            name: "server-option".into(),
1886            value: Some(option.clone()),
1887        });
1888    }
1889    for capability in &options.extra {
1890        if matches!(
1891            capability.name.as_str(),
1892            "agent" | "object-format" | "server-option"
1893        ) {
1894            return Err(GitError::InvalidFormat(format!(
1895                "protocol v2 extra capability duplicates known capability {}",
1896                capability.name
1897            )));
1898        }
1899        encode_protocol_v2_capability(capability)?;
1900        capabilities.push(capability.clone());
1901    }
1902    Ok(capabilities)
1903}
1904
1905pub fn parse_protocol_v2_ls_refs_features(
1906    capabilities: &[Capability],
1907) -> Result<Option<ProtocolV2LsRefsFeatures>> {
1908    let mut ls_refs = None;
1909    for capability in capabilities {
1910        if capability.name != "ls-refs" {
1911            continue;
1912        }
1913        if ls_refs.is_some() {
1914            return Err(GitError::InvalidFormat(
1915                "protocol v2 has duplicate ls-refs capabilities".into(),
1916            ));
1917        }
1918        ls_refs = Some(parse_protocol_v2_ls_refs_feature_value(
1919            capability.value.as_deref(),
1920        )?);
1921    }
1922    Ok(ls_refs)
1923}
1924
1925pub fn encode_protocol_v2_ls_refs_capability(
1926    features: &ProtocolV2LsRefsFeatures,
1927) -> Result<Capability> {
1928    let mut values = Vec::new();
1929    if features.unborn {
1930        values.push("unborn".to_string());
1931    }
1932    for feature in &features.unknown {
1933        validate_protocol_v2_token("ls-refs feature", feature)?;
1934        if feature == "unborn" {
1935            return Err(GitError::InvalidFormat(
1936                "ls-refs unknown features must not duplicate known feature unborn".into(),
1937            ));
1938        }
1939        values.push(feature.clone());
1940    }
1941    Ok(Capability {
1942        name: "ls-refs".into(),
1943        value: (!values.is_empty()).then(|| values.join(" ")),
1944    })
1945}
1946
1947pub fn validate_protocol_v2_ls_refs_request_features(
1948    features: &ProtocolV2LsRefsFeatures,
1949    request: &ProtocolV2LsRefsRequest,
1950) -> Result<()> {
1951    if request.unborn && !features.unborn {
1952        return Err(GitError::InvalidFormat(
1953            "ls-refs request uses unborn without advertised unborn feature".into(),
1954        ));
1955    }
1956    Ok(())
1957}
1958
1959pub fn validate_protocol_v2_ls_refs_command_request(
1960    handshake: &TransportHandshake,
1961    request: &ProtocolV2CommandRequest,
1962) -> Result<ProtocolV2LsRefsRequest> {
1963    validate_protocol_v2_command_request_capabilities(handshake, request)?;
1964    let ls_refs = ProtocolV2LsRefsRequest::from_command_request(request)?;
1965    let features = parse_protocol_v2_ls_refs_features(&handshake.capabilities)?
1966        .ok_or_else(|| GitError::InvalidFormat("ls-refs command was not advertised".into()))?;
1967    validate_protocol_v2_ls_refs_request_features(&features, &ls_refs)?;
1968    Ok(ls_refs)
1969}
1970
1971pub fn parse_protocol_v2_fetch_features(
1972    capabilities: &[Capability],
1973) -> Result<Option<ProtocolV2FetchFeatures>> {
1974    let mut fetch = None;
1975    for capability in capabilities {
1976        if capability.name != "fetch" {
1977            continue;
1978        }
1979        if fetch.is_some() {
1980            return Err(GitError::InvalidFormat(
1981                "protocol v2 has duplicate fetch capabilities".into(),
1982            ));
1983        }
1984        fetch = Some(parse_protocol_v2_fetch_feature_value(
1985            capability.value.as_deref(),
1986        )?);
1987    }
1988    Ok(fetch)
1989}
1990
1991pub fn encode_protocol_v2_fetch_capability(
1992    features: &ProtocolV2FetchFeatures,
1993) -> Result<Capability> {
1994    let mut values = Vec::new();
1995    if features.shallow {
1996        values.push("shallow".to_string());
1997    }
1998    if features.wait_for_done {
1999        values.push("wait-for-done".to_string());
2000    }
2001    if features.filter {
2002        values.push("filter".to_string());
2003    }
2004    if features.ref_in_want {
2005        values.push("ref-in-want".to_string());
2006    }
2007    if features.sideband_all {
2008        values.push("sideband-all".to_string());
2009    }
2010    if features.packfile_uris {
2011        values.push("packfile-uris".to_string());
2012    }
2013    for feature in &features.unknown {
2014        validate_protocol_v2_token("fetch feature", feature)?;
2015        if matches!(
2016            feature.as_str(),
2017            "shallow"
2018                | "wait-for-done"
2019                | "filter"
2020                | "ref-in-want"
2021                | "sideband-all"
2022                | "packfile-uris"
2023        ) {
2024            return Err(GitError::InvalidFormat(format!(
2025                "fetch unknown features must not duplicate known feature {feature}"
2026            )));
2027        }
2028        values.push(feature.clone());
2029    }
2030    Ok(Capability {
2031        name: "fetch".into(),
2032        value: (!values.is_empty()).then(|| values.join(" ")),
2033    })
2034}
2035
2036pub fn validate_protocol_v2_fetch_request_features(
2037    features: &ProtocolV2FetchFeatures,
2038    request: &ProtocolV2FetchRequest,
2039) -> Result<()> {
2040    if !features.shallow
2041        && (!request.shallow.is_empty()
2042            || request.deepen.is_some()
2043            || request.deepen_since.is_some()
2044            || !request.deepen_not.is_empty()
2045            || request.deepen_relative)
2046    {
2047        return Err(GitError::InvalidFormat(
2048            "fetch request uses shallow/deepen arguments without advertised shallow feature".into(),
2049        ));
2050    }
2051    if !features.filter && request.filter.is_some() {
2052        return Err(GitError::InvalidFormat(
2053            "fetch request uses filter without advertised filter feature".into(),
2054        ));
2055    }
2056    if !features.ref_in_want && !request.want_refs.is_empty() {
2057        return Err(GitError::InvalidFormat(
2058            "fetch request uses want-ref without advertised ref-in-want feature".into(),
2059        ));
2060    }
2061    if !features.sideband_all && request.sideband_all {
2062        return Err(GitError::InvalidFormat(
2063            "fetch request uses sideband-all without advertised sideband-all feature".into(),
2064        ));
2065    }
2066    if !features.packfile_uris && request.packfile_uris.is_some() {
2067        return Err(GitError::InvalidFormat(
2068            "fetch request uses packfile-uris without advertised packfile-uris feature".into(),
2069        ));
2070    }
2071    if !features.wait_for_done && request.wait_for_done {
2072        return Err(GitError::InvalidFormat(
2073            "fetch request uses wait-for-done without advertised wait-for-done feature".into(),
2074        ));
2075    }
2076    Ok(())
2077}
2078
2079pub fn validate_protocol_v2_fetch_command_request(
2080    handshake: &TransportHandshake,
2081    format: ObjectFormat,
2082    request: &ProtocolV2CommandRequest,
2083) -> Result<ProtocolV2FetchRequest> {
2084    validate_protocol_v2_command_request_capabilities(handshake, request)?;
2085    let fetch = ProtocolV2FetchRequest::from_command_request(format, request)?;
2086    let features = parse_protocol_v2_fetch_features(&handshake.capabilities)?
2087        .ok_or_else(|| GitError::InvalidFormat("fetch command was not advertised".into()))?;
2088    validate_protocol_v2_fetch_request_features(&features, &fetch)?;
2089    Ok(fetch)
2090}
2091
2092pub fn validate_protocol_v2_object_info_command_request(
2093    handshake: &TransportHandshake,
2094    format: ObjectFormat,
2095    request: &ProtocolV2CommandRequest,
2096) -> Result<ProtocolV2ObjectInfoRequest> {
2097    validate_protocol_v2_command_request_capabilities(handshake, request)?;
2098    let object_info = ProtocolV2ObjectInfoRequest::from_command_request(format, request)?;
2099    protocol_v2_capability(&handshake.capabilities, "object-info")
2100        .ok_or_else(|| GitError::InvalidFormat("object-info command was not advertised".into()))?;
2101    Ok(object_info)
2102}
2103
2104pub fn classify_protocol_v2_command_request(
2105    handshake: &TransportHandshake,
2106    format: ObjectFormat,
2107    request: &ProtocolV2CommandRequest,
2108) -> Result<ProtocolV2Command> {
2109    match request.command.as_str() {
2110        "ls-refs" => validate_protocol_v2_ls_refs_command_request(handshake, request)
2111            .map(ProtocolV2Command::LsRefs),
2112        "fetch" => validate_protocol_v2_fetch_command_request(handshake, format, request)
2113            .map(ProtocolV2Command::Fetch),
2114        "object-info" => {
2115            validate_protocol_v2_object_info_command_request(handshake, format, request)
2116                .map(ProtocolV2Command::ObjectInfo)
2117        }
2118        _ => {
2119            validate_protocol_v2_command_request_capabilities(handshake, request)?;
2120            Ok(ProtocolV2Command::Unknown(request.clone()))
2121        }
2122    }
2123}
2124
2125pub fn classify_protocol_v2_request(
2126    handshake: &TransportHandshake,
2127    format: ObjectFormat,
2128    request: &ProtocolV2Request,
2129) -> Result<ProtocolV2SessionRequest> {
2130    match request {
2131        ProtocolV2Request::Command(command) => {
2132            classify_protocol_v2_command_request(handshake, format, command)
2133                .map(ProtocolV2SessionRequest::Command)
2134        }
2135        ProtocolV2Request::Done => Ok(ProtocolV2SessionRequest::Done),
2136    }
2137}
2138
2139pub fn read_protocol_v2_session_request(
2140    handshake: &TransportHandshake,
2141    format: ObjectFormat,
2142    reader: &mut impl Read,
2143) -> Result<ProtocolV2SessionRequest> {
2144    let request = read_protocol_v2_request(reader)?;
2145    classify_protocol_v2_request(handshake, format, &request)
2146}
2147
2148fn protocol_v2_capability<'a>(
2149    capabilities: &'a [Capability],
2150    name: &str,
2151) -> Option<&'a Capability> {
2152    capabilities
2153        .iter()
2154        .find(|capability| capability.name == name)
2155}
2156
2157fn validate_protocol_v2_object_format_request(
2158    advertised: &Capability,
2159    requested: &Capability,
2160) -> Result<()> {
2161    let Some(advertised) = &advertised.value else {
2162        return Err(GitError::InvalidFormat(
2163            "advertised object-format capability is missing a value".into(),
2164        ));
2165    };
2166    let Some(requested) = &requested.value else {
2167        return Err(GitError::InvalidFormat(
2168            "requested object-format capability is missing a value".into(),
2169        ));
2170    };
2171    if advertised != requested {
2172        return Err(GitError::InvalidFormat(format!(
2173            "requested object-format {requested} does not match advertised {advertised}"
2174        )));
2175    }
2176    Ok(())
2177}
2178
2179fn parse_protocol_v2_ls_refs_feature_value(
2180    value: Option<&str>,
2181) -> Result<ProtocolV2LsRefsFeatures> {
2182    let mut out = ProtocolV2LsRefsFeatures::default();
2183    let Some(value) = value else {
2184        return Ok(out);
2185    };
2186    if value.is_empty() {
2187        return Err(GitError::InvalidFormat(
2188            "protocol v2 ls-refs capability value is empty".into(),
2189        ));
2190    }
2191    for feature in value.split(' ') {
2192        validate_protocol_v2_token("ls-refs feature", feature)?;
2193        match feature {
2194            "unborn" => out.unborn = true,
2195            other => out.unknown.push(other.to_string()),
2196        }
2197    }
2198    Ok(out)
2199}
2200
2201fn parse_protocol_v2_fetch_feature_value(value: Option<&str>) -> Result<ProtocolV2FetchFeatures> {
2202    let mut out = ProtocolV2FetchFeatures::default();
2203    let Some(value) = value else {
2204        return Ok(out);
2205    };
2206    if value.is_empty() {
2207        return Err(GitError::InvalidFormat(
2208            "protocol v2 fetch capability value is empty".into(),
2209        ));
2210    }
2211    for feature in value.split(' ') {
2212        validate_protocol_v2_token("fetch feature", feature)?;
2213        match feature {
2214            "shallow" => out.shallow = true,
2215            "wait-for-done" => out.wait_for_done = true,
2216            "filter" => out.filter = true,
2217            "ref-in-want" => out.ref_in_want = true,
2218            "sideband-all" => out.sideband_all = true,
2219            "packfile-uris" => out.packfile_uris = true,
2220            other => out.unknown.push(other.to_string()),
2221        }
2222    }
2223    Ok(out)
2224}
2225pub(crate) fn parse_protocol_v2_capability_line(payload: &[u8]) -> Result<Capability> {
2226    let payload = trim_trailing_lf(payload);
2227    if payload.is_empty() {
2228        return Err(GitError::InvalidFormat(
2229            "empty protocol v2 capability line".into(),
2230        ));
2231    }
2232    let text =
2233        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
2234    let (name, value) = text
2235        .split_once('=')
2236        .map_or((text, None), |(name, value)| (name, Some(value)));
2237    validate_capability_name(name)?;
2238    if let Some(value) = value {
2239        validate_protocol_v2_capability_value(value)?;
2240    }
2241    Ok(Capability {
2242        name: name.to_string(),
2243        value: value.map(str::to_string),
2244    })
2245}
2246
2247pub(crate) fn parse_protocol_v2_command_line(payload: &[u8]) -> Result<String> {
2248    let payload = trim_trailing_lf(payload);
2249    let text =
2250        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
2251    let Some(command) = text.strip_prefix("command=") else {
2252        return Err(GitError::InvalidFormat(
2253            "protocol v2 command request missing command prefix".into(),
2254        ));
2255    };
2256    validate_capability_name(command)?;
2257    Ok(command.to_string())
2258}
2259
2260fn parse_protocol_v2_ls_refs_line(
2261    format: ObjectFormat,
2262    payload: &[u8],
2263) -> Result<ProtocolV2LsRefsRecord> {
2264    let payload = trim_trailing_lf(payload);
2265    if payload.is_empty() {
2266        return Err(GitError::InvalidFormat(
2267            "ls-refs response line is empty".into(),
2268        ));
2269    }
2270    let text =
2271        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
2272    let mut fields = text.split(' ');
2273    let first = fields
2274        .next()
2275        .ok_or_else(|| GitError::InvalidFormat("ls-refs response line is empty".into()))?;
2276    if first == "unborn" {
2277        let name = fields
2278            .next()
2279            .ok_or_else(|| GitError::InvalidFormat("ls-refs unborn line is missing name".into()))?;
2280        validate_protocol_v2_token("ls-refs ref name", name)?;
2281        let (symref_target, attributes) = parse_protocol_v2_ls_refs_attributes(format, fields)?;
2282        return Ok(ProtocolV2LsRefsRecord::Unborn {
2283            name: name.to_string(),
2284            symref_target,
2285            attributes,
2286        });
2287    }
2288
2289    let oid = ObjectId::from_hex(format, first)?;
2290    let name = fields
2291        .next()
2292        .ok_or_else(|| GitError::InvalidFormat("ls-refs ref line is missing name".into()))?;
2293    validate_protocol_v2_token("ls-refs ref name", name)?;
2294    let (peeled, symref_target, attributes) =
2295        parse_protocol_v2_ls_refs_ref_attributes(format, fields)?;
2296    Ok(ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
2297        oid,
2298        name: name.to_string(),
2299        peeled,
2300        symref_target,
2301        attributes,
2302    }))
2303}
2304
2305fn parse_protocol_v2_ls_refs_ref_attributes<'a>(
2306    format: ObjectFormat,
2307    fields: impl Iterator<Item = &'a str>,
2308) -> Result<(Option<ObjectId>, Option<String>, Vec<String>)> {
2309    let mut peeled = None;
2310    let (symref_target, attributes) =
2311        parse_protocol_v2_ls_refs_attributes_with(format, fields, |attr| {
2312            if let Some(value) = attr.strip_prefix("peeled:") {
2313                if peeled.is_some() {
2314                    return Err(GitError::InvalidFormat(
2315                        "ls-refs response has duplicate peeled attribute".into(),
2316                    ));
2317                }
2318                peeled = Some(ObjectId::from_hex(format, value)?);
2319                return Ok(true);
2320            }
2321            Ok(false)
2322        })?;
2323    Ok((peeled, symref_target, attributes))
2324}
2325
2326fn parse_protocol_v2_ls_refs_attributes<'a>(
2327    format: ObjectFormat,
2328    fields: impl Iterator<Item = &'a str>,
2329) -> Result<(Option<String>, Vec<String>)> {
2330    parse_protocol_v2_ls_refs_attributes_with(format, fields, |attr| {
2331        if attr.starts_with("peeled:") {
2332            return Err(GitError::InvalidFormat(
2333                "ls-refs unborn line has peeled attribute".into(),
2334            ));
2335        }
2336        Ok(false)
2337    })
2338}
2339
2340fn parse_protocol_v2_ls_refs_attributes_with<'a, F>(
2341    _format: ObjectFormat,
2342    fields: impl Iterator<Item = &'a str>,
2343    mut handle_known: F,
2344) -> Result<(Option<String>, Vec<String>)>
2345where
2346    F: FnMut(&str) -> Result<bool>,
2347{
2348    let mut symref_target = None;
2349    let mut attributes = Vec::new();
2350    for attr in fields {
2351        validate_protocol_v2_token("ls-refs attribute", attr)?;
2352        if let Some(value) = attr.strip_prefix("symref-target:") {
2353            if symref_target.is_some() {
2354                return Err(GitError::InvalidFormat(
2355                    "ls-refs response has duplicate symref-target attribute".into(),
2356                ));
2357            }
2358            validate_protocol_v2_token("ls-refs symref-target", value)?;
2359            symref_target = Some(value.to_string());
2360        } else if !handle_known(attr)? {
2361            attributes.push(attr.to_string());
2362        }
2363    }
2364    Ok((symref_target, attributes))
2365}
2366
2367fn format_protocol_v2_ls_refs_record(record: &ProtocolV2LsRefsRecord) -> Result<String> {
2368    let mut out = String::new();
2369    match record {
2370        ProtocolV2LsRefsRecord::Ref(reference) => {
2371            validate_protocol_v2_token("ls-refs ref name", &reference.name)?;
2372            out.push_str(&reference.oid.to_string());
2373            out.push(' ');
2374            out.push_str(&reference.name);
2375            if let Some(peeled) = &reference.peeled {
2376                if peeled.format() != reference.oid.format() {
2377                    return Err(GitError::InvalidObjectId(
2378                        "ls-refs peeled object format does not match ref object format".into(),
2379                    ));
2380                }
2381                out.push(' ');
2382                out.push_str("peeled:");
2383                out.push_str(&peeled.to_string());
2384            }
2385            if let Some(target) = &reference.symref_target {
2386                validate_protocol_v2_token("ls-refs symref-target", target)?;
2387                out.push(' ');
2388                out.push_str("symref-target:");
2389                out.push_str(target);
2390            }
2391            append_protocol_v2_ls_refs_attributes(&mut out, &reference.attributes)?;
2392        }
2393        ProtocolV2LsRefsRecord::Unborn {
2394            name,
2395            symref_target,
2396            attributes,
2397        } => {
2398            validate_protocol_v2_token("ls-refs ref name", name)?;
2399            out.push_str("unborn ");
2400            out.push_str(name);
2401            if let Some(target) = symref_target {
2402                validate_protocol_v2_token("ls-refs symref-target", target)?;
2403                out.push(' ');
2404                out.push_str("symref-target:");
2405                out.push_str(target);
2406            }
2407            append_protocol_v2_ls_refs_attributes(&mut out, attributes)?;
2408        }
2409    }
2410    Ok(out)
2411}
2412
2413fn append_protocol_v2_ls_refs_attributes(out: &mut String, attributes: &[String]) -> Result<()> {
2414    for attr in attributes {
2415        validate_protocol_v2_token("ls-refs attribute", attr)?;
2416        if attr.starts_with("peeled:") || attr.starts_with("symref-target:") {
2417            return Err(GitError::InvalidFormat(
2418                "ls-refs generic attributes must not duplicate known attributes".into(),
2419            ));
2420        }
2421        out.push(' ');
2422        out.push_str(attr);
2423    }
2424    Ok(())
2425}
2426
2427fn parse_fetch_section_header(payload: &[u8]) -> Result<String> {
2428    let name = parse_protocol_v2_line_text("fetch response section", payload)?;
2429    validate_capability_name(name)?;
2430    Ok(name.to_string())
2431}
2432
2433fn flush_terminates_protocol_v2_response(frames: &[PktLineFrame], idx: usize) -> bool {
2434    idx + 1 == frames.len()
2435        || (idx + 2 == frames.len() && matches!(frames[idx + 1], PktLineFrame::ResponseEnd))
2436}
2437
2438fn parse_fetch_section(
2439    format: ObjectFormat,
2440    name: String,
2441    lines: Vec<Vec<u8>>,
2442) -> Result<ProtocolV2FetchResponseSection> {
2443    match name.as_str() {
2444        "acknowledgments" => lines
2445            .iter()
2446            .map(|line| parse_fetch_acknowledgment(format, line))
2447            .collect::<Result<Vec<_>>>()
2448            .map(ProtocolV2FetchResponseSection::Acknowledgments),
2449        "shallow-info" => lines
2450            .iter()
2451            .map(|line| parse_fetch_shallow_info(format, line))
2452            .collect::<Result<Vec<_>>>()
2453            .map(ProtocolV2FetchResponseSection::ShallowInfo),
2454        "wanted-refs" => lines
2455            .iter()
2456            .map(|line| parse_fetch_wanted_ref(format, line))
2457            .collect::<Result<Vec<_>>>()
2458            .map(ProtocolV2FetchResponseSection::WantedRefs),
2459        "packfile-uris" => lines
2460            .iter()
2461            .map(|line| parse_fetch_packfile_uri(format, line))
2462            .collect::<Result<Vec<_>>>()
2463            .map(ProtocolV2FetchResponseSection::PackfileUris),
2464        "packfile" => Ok(ProtocolV2FetchResponseSection::Packfile(lines)),
2465        _ => Ok(ProtocolV2FetchResponseSection::Unknown { name, lines }),
2466    }
2467}
2468
2469fn parse_fetch_acknowledgment(
2470    format: ObjectFormat,
2471    line: &[u8],
2472) -> Result<ProtocolV2FetchAcknowledgment> {
2473    let text = parse_protocol_v2_line_text("fetch acknowledgment", line)?;
2474    match text {
2475        "NAK" => Ok(ProtocolV2FetchAcknowledgment::Nak),
2476        "ready" => Ok(ProtocolV2FetchAcknowledgment::Ready),
2477        value if value.starts_with("ACK ") => Ok(ProtocolV2FetchAcknowledgment::Ack(
2478            parse_oid_argument(format, "fetch ACK", value, "ACK ")?,
2479        )),
2480        other => Err(GitError::InvalidFormat(format!(
2481            "unsupported fetch acknowledgment {other}"
2482        ))),
2483    }
2484}
2485
2486pub(crate) fn parse_fetch_shallow_info(
2487    format: ObjectFormat,
2488    line: &[u8],
2489) -> Result<ProtocolV2FetchShallowInfo> {
2490    let text = parse_protocol_v2_line_text("fetch shallow-info", line)?;
2491    if text.starts_with("shallow ") {
2492        return Ok(ProtocolV2FetchShallowInfo::Shallow(parse_oid_argument(
2493            format,
2494            "fetch shallow",
2495            text,
2496            "shallow ",
2497        )?));
2498    }
2499    if text.starts_with("unshallow ") {
2500        return Ok(ProtocolV2FetchShallowInfo::Unshallow(parse_oid_argument(
2501            format,
2502            "fetch unshallow",
2503            text,
2504            "unshallow ",
2505        )?));
2506    }
2507    Err(GitError::InvalidFormat(format!(
2508        "unsupported fetch shallow-info {text}"
2509    )))
2510}
2511
2512fn parse_fetch_wanted_ref(format: ObjectFormat, line: &[u8]) -> Result<ProtocolV2FetchWantedRef> {
2513    let text = parse_protocol_v2_line_text("fetch wanted-ref", line)?;
2514    let (oid, name) = text
2515        .split_once(' ')
2516        .ok_or_else(|| GitError::InvalidFormat("fetch wanted-ref is missing name".into()))?;
2517    validate_protocol_v2_token("fetch wanted-ref name", name)?;
2518    Ok(ProtocolV2FetchWantedRef {
2519        oid: ObjectId::from_hex(format, oid)?,
2520        name: name.to_string(),
2521    })
2522}
2523
2524fn parse_fetch_packfile_uri(
2525    format: ObjectFormat,
2526    line: &[u8],
2527) -> Result<ProtocolV2FetchPackfileUri> {
2528    let text = parse_protocol_v2_line_text("fetch packfile-uri", line)?;
2529    let (pack_hash, uri) = text
2530        .split_once(' ')
2531        .ok_or_else(|| GitError::InvalidFormat("fetch packfile-uri is missing uri".into()))?;
2532    validate_protocol_v2_token("fetch packfile-uri hash", pack_hash)?;
2533    validate_protocol_v2_token("fetch packfile-uri", uri)?;
2534    Ok(ProtocolV2FetchPackfileUri {
2535        pack_hash: ObjectId::from_hex(format, pack_hash)?,
2536        uri: uri.to_string(),
2537    })
2538}
2539
2540fn protocol_v2_fetch_section_name(section: &ProtocolV2FetchResponseSection) -> &str {
2541    match section {
2542        ProtocolV2FetchResponseSection::Acknowledgments(_) => "acknowledgments",
2543        ProtocolV2FetchResponseSection::ShallowInfo(_) => "shallow-info",
2544        ProtocolV2FetchResponseSection::WantedRefs(_) => "wanted-refs",
2545        ProtocolV2FetchResponseSection::PackfileUris(_) => "packfile-uris",
2546        ProtocolV2FetchResponseSection::Packfile(_) => "packfile",
2547        ProtocolV2FetchResponseSection::Unknown { name, .. } => name,
2548    }
2549}
2550
2551fn format_protocol_v2_fetch_section_lines(
2552    section: &ProtocolV2FetchResponseSection,
2553) -> Result<Vec<Vec<u8>>> {
2554    match section {
2555        ProtocolV2FetchResponseSection::Acknowledgments(acks) => acks
2556            .iter()
2557            .map(|ack| match ack {
2558                ProtocolV2FetchAcknowledgment::Nak => Ok(line_from_str("NAK")),
2559                ProtocolV2FetchAcknowledgment::Ack(oid) => Ok(line_from_str(&format!("ACK {oid}"))),
2560                ProtocolV2FetchAcknowledgment::Ready => Ok(line_from_str("ready")),
2561            })
2562            .collect(),
2563        ProtocolV2FetchResponseSection::ShallowInfo(entries) => entries
2564            .iter()
2565            .map(|entry| match entry {
2566                // Unlike most protocol-v2 text records, upload-pack writes
2567                // shallow-info entries without a trailing LF. The pkt-line
2568                // length is therefore 0034/0036 for SHA-1 and 004c/004e for
2569                // SHA-256, matching Git's byte-level wire contract.
2570                ProtocolV2FetchShallowInfo::Shallow(oid) => {
2571                    Ok(format!("shallow {oid}").into_bytes())
2572                }
2573                ProtocolV2FetchShallowInfo::Unshallow(oid) => {
2574                    Ok(format!("unshallow {oid}").into_bytes())
2575                }
2576            })
2577            .collect(),
2578        ProtocolV2FetchResponseSection::WantedRefs(refs) => refs
2579            .iter()
2580            .map(|wanted| {
2581                validate_protocol_v2_token("fetch wanted-ref name", &wanted.name)?;
2582                Ok(line_from_str(&format!("{} {}", wanted.oid, wanted.name)))
2583            })
2584            .collect(),
2585        ProtocolV2FetchResponseSection::PackfileUris(uris) => uris
2586            .iter()
2587            .map(|packfile_uri| {
2588                validate_protocol_v2_token("fetch packfile-uri", &packfile_uri.uri)?;
2589                Ok(line_from_str(&format!(
2590                    "{} {}",
2591                    packfile_uri.pack_hash, packfile_uri.uri
2592                )))
2593            })
2594            .collect(),
2595        ProtocolV2FetchResponseSection::Packfile(lines) => Ok(lines.clone()),
2596        ProtocolV2FetchResponseSection::Unknown { name, lines } => {
2597            validate_capability_name(name)?;
2598            for line in lines {
2599                validate_protocol_v2_line("fetch unknown section line", line)?;
2600            }
2601            Ok(lines.clone())
2602        }
2603    }
2604}
2605
2606fn parse_protocol_v2_object_info_record(
2607    format: ObjectFormat,
2608    line: &[u8],
2609) -> Result<ProtocolV2ObjectInfoRecord> {
2610    let text = parse_protocol_v2_line_text("object-info record", line)?;
2611    let mut fields = text.split(' ');
2612    let oid = fields
2613        .next()
2614        .ok_or_else(|| GitError::InvalidFormat("object-info record is missing oid".into()))?;
2615    let size = fields
2616        .next()
2617        .ok_or_else(|| GitError::InvalidFormat("object-info record is missing size".into()))?;
2618    if fields.next().is_some() {
2619        return Err(GitError::InvalidFormat(
2620            "object-info record has too many fields".into(),
2621        ));
2622    }
2623    validate_protocol_v2_token("object-info oid", oid)?;
2624    validate_protocol_v2_token("object-info size", size)?;
2625    let size = size
2626        .parse::<u64>()
2627        .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
2628    Ok(ProtocolV2ObjectInfoRecord {
2629        oid: ObjectId::from_hex(format, oid)?,
2630        size,
2631    })
2632}
2633
2634pub(crate) fn encode_protocol_v2_capability(capability: &Capability) -> Result<Vec<u8>> {
2635    validate_capability_name(&capability.name)?;
2636    let mut out = capability.name.as_bytes().to_vec();
2637    if let Some(value) = &capability.value {
2638        validate_protocol_v2_capability_value(value)?;
2639        out.push(b'=');
2640        out.extend_from_slice(value.as_bytes());
2641    }
2642    Ok(out)
2643}
2644
2645pub(crate) fn validate_protocol_v2_capability_value(value: &str) -> Result<()> {
2646    if value.is_empty() {
2647        return Err(GitError::InvalidFormat(
2648            "protocol v2 capability value is empty".into(),
2649        ));
2650    }
2651    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
2652        return Err(GitError::InvalidFormat(
2653            "protocol v2 capability value contains a delimiter byte".into(),
2654        ));
2655    }
2656    Ok(())
2657}
2658
2659fn validate_protocol_v2_argument(value: &[u8]) -> Result<()> {
2660    if value.is_empty() {
2661        return Err(GitError::InvalidFormat(
2662            "protocol v2 command argument is empty".into(),
2663        ));
2664    }
2665    if value.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
2666        return Err(GitError::InvalidFormat(
2667            "protocol v2 command argument contains a delimiter byte".into(),
2668        ));
2669    }
2670    Ok(())
2671}
2672
2673pub(crate) fn parse_u32_argument(label: &str, value: &str, prefix: &str) -> Result<u32> {
2674    let number = value
2675        .strip_prefix(prefix)
2676        .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
2677    validate_protocol_v2_token(label, number)?;
2678    let parsed = number
2679        .parse::<u32>()
2680        .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
2681    if parsed == 0 {
2682        return Err(GitError::InvalidFormat(format!("{label} must be positive")));
2683    }
2684    Ok(parsed)
2685}
2686
2687pub(crate) fn parse_u64_argument(label: &str, value: &str, prefix: &str) -> Result<u64> {
2688    let number = value
2689        .strip_prefix(prefix)
2690        .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
2691    validate_protocol_v2_token(label, number)?;
2692    number
2693        .parse::<u64>()
2694        .map_err(|err| GitError::InvalidFormat(err.to_string()))
2695}