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