Skip to main content

sley_protocol/
lib.rs

1// sley#7: untrusted-input parsing crate — fallible ops propagate errors;
2// the only retained `expect`s would be documented compile-time invariants.
3#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
4
5use sley_core::{Capability, GitError, ObjectFormat, ObjectId, Result};
6use std::io::{ErrorKind, Read, Write};
7
8pub const PKT_LINE_MAX_LEN: usize = 65_520;
9
10pub const PKT_LINE_MAX_PAYLOAD_LEN: usize = PKT_LINE_MAX_LEN - 4;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ProtocolVersion {
14    V0,
15    V1,
16    V2,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct PktLine(pub Vec<u8>);
21
22impl PktLine {
23    pub fn encode(&self) -> Vec<u8> {
24        encode_pkt_line_payload(&self.0)
25    }
26
27    pub fn try_encode(&self) -> Result<Vec<u8>> {
28        validate_pkt_line_payload(&self.0)?;
29        Ok(self.encode())
30    }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum PktLineFrame {
35    Data(Vec<u8>),
36    Flush,
37    Delimiter,
38    ResponseEnd,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct ProtocolErrorLine {
43    pub message: String,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum GitService {
48    UploadPack,
49    ReceivePack,
50    UploadArchive,
51}
52
53impl GitService {
54    pub fn as_str(self) -> &'static str {
55        match self {
56            Self::UploadPack => "git-upload-pack",
57            Self::ReceivePack => "git-receive-pack",
58            Self::UploadArchive => "git-upload-archive",
59        }
60    }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct RefSpec {
65    pub force: bool,
66    pub negative: bool,
67    pub src: Option<String>,
68    pub dst: Option<String>,
69    pub pattern: bool,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct FetchHeadRecord {
74    pub oid: ObjectId,
75    pub not_for_merge: bool,
76    pub description: String,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct FetchRefUpdate {
81    pub src: String,
82    pub dst: Option<String>,
83    pub oid: ObjectId,
84    pub not_for_merge: bool,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct PushSourceRef {
89    pub name: String,
90    pub oid: ObjectId,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum SideBandChannel {
95    Data,
96    Progress,
97    Fatal,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct SideBandPacket {
102    pub channel: SideBandChannel,
103    pub data: Vec<u8>,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Default)]
107pub struct SideBandDemux {
108    pub data: Vec<u8>,
109    pub progress: Vec<Vec<u8>>,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Default)]
113pub struct UploadArchiveRequest {
114    pub arguments: Vec<String>,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum UploadArchiveResponse {
119    Ack { sideband: Vec<SideBandPacket> },
120    Nack { message: String },
121}
122
123impl PktLineFrame {
124    pub fn data(payload: impl Into<Vec<u8>>) -> Result<Self> {
125        let payload = payload.into();
126        validate_pkt_line_payload(&payload)?;
127        Ok(Self::Data(payload))
128    }
129
130    pub fn encode(&self) -> Vec<u8> {
131        match self {
132            Self::Data(payload) => encode_pkt_line_payload(payload),
133            Self::Flush => b"0000".to_vec(),
134            Self::Delimiter => b"0001".to_vec(),
135            Self::ResponseEnd => b"0002".to_vec(),
136        }
137    }
138
139    pub fn try_encode(&self) -> Result<Vec<u8>> {
140        match self {
141            Self::Data(payload) => try_encode_pkt_line_payload(payload),
142            Self::Flush | Self::Delimiter | Self::ResponseEnd => Ok(self.encode()),
143        }
144    }
145
146    pub fn parse(input: &[u8]) -> Result<(Self, usize)> {
147        if input.len() < 4 {
148            return Err(GitError::InvalidFormat("truncated pkt-line length".into()));
149        }
150        let len = parse_pkt_len(&input[..4])?;
151        match len {
152            0 => Ok((Self::Flush, 4)),
153            1 => Ok((Self::Delimiter, 4)),
154            2 => Ok((Self::ResponseEnd, 4)),
155            3 => Err(GitError::InvalidFormat(
156                "reserved pkt-line length 0003".into(),
157            )),
158            4..=PKT_LINE_MAX_LEN => {
159                if input.len() < len {
160                    return Err(GitError::InvalidFormat(format!(
161                        "truncated pkt-line payload: expected {} bytes, got {}",
162                        len - 4,
163                        input.len().saturating_sub(4)
164                    )));
165                }
166                Ok((Self::Data(input[4..len].to_vec()), len))
167            }
168            _ => Err(GitError::InvalidFormat(format!(
169                "pkt-line length exceeds {PKT_LINE_MAX_LEN}: {len}"
170            ))),
171        }
172    }
173}
174
175fn validate_pkt_line_payload(payload: &[u8]) -> Result<()> {
176    if payload.len() > PKT_LINE_MAX_PAYLOAD_LEN {
177        return Err(GitError::InvalidFormat(format!(
178            "pkt-line payload exceeds {PKT_LINE_MAX_PAYLOAD_LEN} bytes"
179        )));
180    }
181    Ok(())
182}
183
184fn pkt_line_header(len: usize) -> [u8; 4] {
185    const HEX: &[u8; 16] = b"0123456789abcdef";
186    [
187        HEX[(len >> 12) & 0xf],
188        HEX[(len >> 8) & 0xf],
189        HEX[(len >> 4) & 0xf],
190        HEX[len & 0xf],
191    ]
192}
193
194fn encode_pkt_line_payload(payload: &[u8]) -> Vec<u8> {
195    let len = payload.len() + 4;
196    let mut out = Vec::with_capacity(len);
197    out.extend_from_slice(&pkt_line_header(len));
198    out.extend_from_slice(payload);
199    out
200}
201
202fn try_encode_pkt_line_payload(payload: &[u8]) -> Result<Vec<u8>> {
203    validate_pkt_line_payload(payload)?;
204    Ok(encode_pkt_line_payload(payload))
205}
206
207pub fn parse_pkt_line_stream(mut input: &[u8]) -> Result<Vec<PktLineFrame>> {
208    let mut frames = Vec::new();
209    while !input.is_empty() {
210        let (frame, consumed) = PktLineFrame::parse(input)?;
211        frames.push(frame);
212        input = &input[consumed..];
213    }
214    Ok(frames)
215}
216
217fn parse_pkt_line_frames_until_flush_from(mut input: &[u8]) -> Result<(Vec<PktLineFrame>, usize)> {
218    let mut frames = Vec::new();
219    let mut total = 0usize;
220    loop {
221        if input.is_empty() {
222            return Err(GitError::InvalidFormat(
223                "pkt-line stream ended before flush".into(),
224            ));
225        }
226        let (frame, consumed) = PktLineFrame::parse(input)?;
227        total += consumed;
228        let done = matches!(frame, PktLineFrame::Flush);
229        frames.push(frame);
230        input = &input[consumed..];
231        if done {
232            return Ok((frames, total));
233        }
234    }
235}
236
237pub fn read_pkt_line_frame(reader: &mut impl Read) -> Result<Option<PktLineFrame>> {
238    let mut header = [0u8; 4];
239    let mut read = 0usize;
240    while read < header.len() {
241        match reader.read(&mut header[read..]) {
242            Ok(0) if read == 0 => return Ok(None),
243            Ok(0) => {
244                return Err(GitError::InvalidFormat("truncated pkt-line length".into()));
245            }
246            Ok(n) => read += n,
247            Err(err) if err.kind() == ErrorKind::Interrupted => {}
248            Err(err) => return Err(err.into()),
249        }
250    }
251
252    let len = parse_pkt_len(&header)?;
253    match len {
254        0 => Ok(Some(PktLineFrame::Flush)),
255        1 => Ok(Some(PktLineFrame::Delimiter)),
256        2 => Ok(Some(PktLineFrame::ResponseEnd)),
257        3 => Err(GitError::InvalidFormat(
258            "reserved pkt-line length 0003".into(),
259        )),
260        4..=PKT_LINE_MAX_LEN => {
261            let mut payload = vec![0; len - 4];
262            reader.read_exact(&mut payload)?;
263            Ok(Some(PktLineFrame::Data(payload)))
264        }
265        _ => Err(GitError::InvalidFormat(format!(
266            "pkt-line length exceeds {PKT_LINE_MAX_LEN}: {len}"
267        ))),
268    }
269}
270
271pub fn read_pkt_line_frames(reader: &mut impl Read) -> Result<Vec<PktLineFrame>> {
272    let mut frames = Vec::new();
273    while let Some(frame) = read_pkt_line_frame(reader)? {
274        frames.push(frame);
275    }
276    Ok(frames)
277}
278
279pub fn read_pkt_line_frames_until_flush(reader: &mut impl Read) -> Result<Vec<PktLineFrame>> {
280    read_pkt_line_frames_until_control(reader, |frame| matches!(frame, PktLineFrame::Flush))
281}
282
283pub fn read_pkt_line_frames_until_response_end(
284    reader: &mut impl Read,
285) -> Result<Vec<PktLineFrame>> {
286    read_pkt_line_frames_until_control(reader, |frame| matches!(frame, PktLineFrame::ResponseEnd))
287}
288
289fn read_pkt_line_frames_until_control(
290    reader: &mut impl Read,
291    stop: impl Fn(&PktLineFrame) -> bool,
292) -> Result<Vec<PktLineFrame>> {
293    let mut frames = Vec::new();
294    loop {
295        let Some(frame) = read_pkt_line_frame(reader)? else {
296            return Err(GitError::InvalidFormat(
297                "pkt-line stream ended before control packet".into(),
298            ));
299        };
300        let done = stop(&frame);
301        frames.push(frame);
302        if done {
303            return Ok(frames);
304        }
305    }
306}
307
308pub fn write_pkt_line_frame(writer: &mut impl Write, frame: &PktLineFrame) -> Result<()> {
309    match frame {
310        PktLineFrame::Data(payload) => write_pkt_line_payload(writer, payload)?,
311        PktLineFrame::Flush => writer.write_all(b"0000")?,
312        PktLineFrame::Delimiter => writer.write_all(b"0001")?,
313        PktLineFrame::ResponseEnd => writer.write_all(b"0002")?,
314    }
315    Ok(())
316}
317
318pub fn write_pkt_line_payload(writer: &mut impl Write, payload: &[u8]) -> Result<()> {
319    validate_pkt_line_payload(payload)?;
320    let len = payload.len() + 4;
321    writer.write_all(&pkt_line_header(len))?;
322    writer.write_all(payload)?;
323    Ok(())
324}
325
326pub fn write_pkt_line_frames(writer: &mut impl Write, frames: &[PktLineFrame]) -> Result<()> {
327    for frame in frames {
328        write_pkt_line_frame(writer, frame)?;
329    }
330    Ok(())
331}
332
333pub fn parse_error_line(payload: &[u8]) -> Result<ProtocolErrorLine> {
334    let text = parse_protocol_v2_line_text("protocol error line", payload)?;
335    let Some(message) = text.strip_prefix("ERR ") else {
336        return Err(GitError::InvalidFormat(
337            "protocol error line must start with ERR".into(),
338        ));
339    };
340    validate_protocol_error_message(message)?;
341    Ok(ProtocolErrorLine {
342        message: message.to_string(),
343    })
344}
345
346pub fn encode_error_line(error: &ProtocolErrorLine) -> Result<Vec<u8>> {
347    validate_protocol_error_message(&error.message)?;
348    Ok(line_from_str(&format!("ERR {}", error.message)))
349}
350
351pub fn parse_error_frame(frame: &PktLineFrame) -> Result<Option<ProtocolErrorLine>> {
352    match frame {
353        PktLineFrame::Data(payload) if trim_trailing_lf(payload).starts_with(b"ERR ") => {
354            parse_error_line(payload).map(Some)
355        }
356        PktLineFrame::Data(_)
357        | PktLineFrame::Flush
358        | PktLineFrame::Delimiter
359        | PktLineFrame::ResponseEnd => Ok(None),
360    }
361}
362
363pub fn read_error_line(reader: &mut impl Read) -> Result<ProtocolErrorLine> {
364    let Some(frame) = read_pkt_line_frame(reader)? else {
365        return Err(GitError::InvalidFormat(
366            "pkt-line stream ended before protocol error line".into(),
367        ));
368    };
369    match frame {
370        PktLineFrame::Data(payload) => parse_error_line(&payload),
371        _ => Err(GitError::InvalidFormat(
372            "protocol error line must be a data packet".into(),
373        )),
374    }
375}
376
377pub fn write_error_line(writer: &mut impl Write, error: &ProtocolErrorLine) -> Result<()> {
378    write_pkt_line_frame(writer, &PktLineFrame::data(encode_error_line(error)?)?)
379}
380
381pub fn parse_git_service(value: &str) -> Result<GitService> {
382    match value {
383        "git-upload-pack" => Ok(GitService::UploadPack),
384        "git-receive-pack" => Ok(GitService::ReceivePack),
385        "git-upload-archive" => Ok(GitService::UploadArchive),
386        other => Err(GitError::InvalidFormat(format!(
387            "unsupported git service {other}"
388        ))),
389    }
390}
391
392pub fn parse_refspec(value: &str) -> Result<RefSpec> {
393    validate_refspec_value(value)?;
394    let (force, value) = value
395        .strip_prefix('+')
396        .map_or((false, value), |value| (true, value));
397    let (negative, value) = value
398        .strip_prefix('^')
399        .map_or((false, value), |value| (true, value));
400    if force && negative {
401        return Err(GitError::InvalidFormat(
402            "negative refspec must not be forced".into(),
403        ));
404    }
405    let (src, dst) = if negative {
406        if value.contains(':') {
407            return Err(GitError::InvalidFormat(
408                "negative refspec must not have a destination".into(),
409            ));
410        }
411        (Some(value), None)
412    } else if let Some((src, dst)) = value.split_once(':') {
413        (non_empty(src), non_empty(dst))
414    } else {
415        (Some(value), None)
416    };
417    if src.is_none() && dst.is_none() && value != ":" {
418        return Err(GitError::InvalidFormat(
419            "refspec must include a source or destination".into(),
420        ));
421    }
422    if negative && src.is_none() {
423        return Err(GitError::InvalidFormat(
424            "negative refspec is missing a source".into(),
425        ));
426    }
427    if let Some(src) = src {
428        validate_refspec_endpoint("refspec source", src)?;
429    }
430    if let Some(dst) = dst {
431        validate_refspec_endpoint("refspec destination", dst)?;
432    }
433    let src_pattern_count = src.map(count_refspec_wildcards).unwrap_or(0);
434    let dst_pattern_count = dst.map(count_refspec_wildcards).unwrap_or(0);
435    if src_pattern_count > 1 || dst_pattern_count > 1 {
436        return Err(GitError::InvalidFormat(
437            "refspec endpoint has too many wildcards".into(),
438        ));
439    }
440    if dst.is_some() && (src_pattern_count == 1) != (dst_pattern_count == 1) {
441        return Err(GitError::InvalidFormat(
442            "refspec wildcard must appear in both source and destination".into(),
443        ));
444    }
445    Ok(RefSpec {
446        force,
447        negative,
448        src: src.map(str::to_string),
449        dst: dst.map(str::to_string),
450        pattern: src_pattern_count == 1 || dst_pattern_count == 1,
451    })
452}
453
454pub fn encode_refspec(refspec: &RefSpec) -> Result<String> {
455    validate_refspec_shape(refspec)?;
456    let mut out = String::new();
457    if refspec.force {
458        out.push('+');
459    }
460    if refspec.negative {
461        out.push('^');
462    }
463    if let Some(src) = &refspec.src {
464        out.push_str(src);
465    }
466    if !refspec.negative && refspec.src.is_none() && refspec.dst.is_none() {
467        out.push(':');
468    } else if !refspec.negative && refspec.dst.is_some() {
469        out.push(':');
470        if let Some(dst) = &refspec.dst {
471            out.push_str(dst);
472        }
473    }
474    Ok(out)
475}
476
477pub fn refspec_matches_source(refspec: &RefSpec, source: &str) -> Result<bool> {
478    Ok(refspec_map_source(refspec, source)?.is_some())
479}
480
481pub fn refspec_map_source(refspec: &RefSpec, source: &str) -> Result<Option<String>> {
482    validate_refspec_shape(refspec)?;
483    validate_refspec_endpoint("refspec match source", source)?;
484    let Some(src) = refspec.src.as_deref() else {
485        return Ok(None);
486    };
487    if refspec.pattern {
488        let Some((src_prefix, src_suffix)) = src.split_once('*') else {
489            return Ok(None);
490        };
491        let Some(middle) = source
492            .strip_prefix(src_prefix)
493            .and_then(|value| value.strip_suffix(src_suffix))
494        else {
495            return Ok(None);
496        };
497        if let Some(dst) = refspec.dst.as_deref() {
498            let (dst_prefix, dst_suffix) = dst.split_once('*').ok_or_else(|| {
499                GitError::InvalidFormat("pattern refspec destination is missing wildcard".into())
500            })?;
501            return Ok(Some(format!("{dst_prefix}{middle}{dst_suffix}")));
502        }
503        return Ok(Some(source.to_string()));
504    }
505    if src == source {
506        return Ok(Some(
507            refspec.dst.clone().unwrap_or_else(|| source.to_string()),
508        ));
509    }
510    Ok(None)
511}
512
513pub fn fetch_head_ref_description(refname: &str) -> Result<String> {
514    validate_fetch_head_description_field(refname)?;
515    // Mirror git's `kind`/`what` split in builtin/fetch.c: `HEAD` yields an empty
516    // note (no `'…' of` prefix at all), the standard ref namespaces get their
517    // kind word, and any other ref name is quoted bare.
518    if refname == "HEAD" {
519        Ok(String::new())
520    } else if let Some(branch) = refname.strip_prefix("refs/heads/") {
521        Ok(format!("branch '{branch}'"))
522    } else if let Some(tag) = refname.strip_prefix("refs/tags/") {
523        Ok(format!("tag '{tag}'"))
524    } else if let Some(rest) = refname.strip_prefix("refs/remotes/") {
525        Ok(format!("remote-tracking branch '{rest}'"))
526    } else {
527        Ok(format!("'{refname}'"))
528    }
529}
530
531pub fn fetch_head_remote_description(refname: &str, remote: &str) -> Result<String> {
532    validate_fetch_head_description_field(remote)?;
533    // git only appends `of <url>` when the note (`what`) is non-empty; a bare
534    // `HEAD` fetch records just the URL with an empty description.
535    let what = fetch_head_ref_description(refname)?;
536    if what.is_empty() {
537        Ok(remote.to_string())
538    } else {
539        Ok(format!("{what} of {remote}"))
540    }
541}
542
543pub fn parse_fetch_head(format: ObjectFormat, input: &[u8]) -> Result<Vec<FetchHeadRecord>> {
544    if input.is_empty() {
545        return Ok(Vec::new());
546    }
547    input
548        .split_inclusive(|byte| *byte == b'\n')
549        .map(|line| parse_fetch_head_record(format, line))
550        .collect()
551}
552
553pub fn encode_fetch_head(records: &[FetchHeadRecord]) -> Result<Vec<u8>> {
554    let mut out = Vec::new();
555    for record in records {
556        validate_fetch_head_description_field(&record.description)?;
557        out.extend_from_slice(record.oid.to_string().as_bytes());
558        out.push(b'\t');
559        if record.not_for_merge {
560            out.extend_from_slice(b"not-for-merge");
561        }
562        out.push(b'\t');
563        out.extend_from_slice(record.description.as_bytes());
564        out.push(b'\n');
565    }
566    Ok(out)
567}
568
569pub fn read_fetch_head(
570    format: ObjectFormat,
571    reader: &mut impl Read,
572) -> Result<Vec<FetchHeadRecord>> {
573    let mut input = Vec::new();
574    reader.read_to_end(&mut input)?;
575    parse_fetch_head(format, &input)
576}
577
578pub fn write_fetch_head(writer: &mut impl Write, records: &[FetchHeadRecord]) -> Result<()> {
579    for record in records {
580        validate_fetch_head_description_field(&record.description)?;
581        writer.write_all(record.oid.to_string().as_bytes())?;
582        writer.write_all(b"\t")?;
583        if record.not_for_merge {
584            writer.write_all(b"not-for-merge")?;
585        }
586        writer.write_all(b"\t")?;
587        writer.write_all(record.description.as_bytes())?;
588        writer.write_all(b"\n")?;
589    }
590    Ok(())
591}
592
593/// Match an abbreviated refspec source against the advertised refs the way
594/// upstream's `find_ref_by_name_abbrev` (remote.c) does: score each
595/// advertisement with `refname_match`'s `ref_rev_parse_rules` (exact name
596/// first, then `refs/<name>`, `refs/tags/<name>`, `refs/heads/<name>`,
597/// `refs/remotes/<name>`, `refs/remotes/<name>/HEAD`) and keep the best.
598fn find_advertised_ref_by_name_abbrev<'a>(
599    refs: &'a [RefAdvertisement],
600    name: &str,
601) -> Option<&'a RefAdvertisement> {
602    let mut best: Option<(&RefAdvertisement, usize)> = None;
603    for reference in refs {
604        let score = fetch_refname_match_score(name, &reference.name);
605        if score > best.map(|(_, score)| score).unwrap_or(0) {
606            best = Some((reference, score));
607        }
608    }
609    best.map(|(reference, _)| reference)
610}
611
612/// `refname_match` (refs.c): non-zero when `abbrev` can mean `full`, with the
613/// magnitude giving disambiguation precedence (earlier rules win).
614fn fetch_refname_match_score(abbrev: &str, full: &str) -> usize {
615    let expansions = [
616        abbrev.to_string(),
617        format!("refs/{abbrev}"),
618        format!("refs/tags/{abbrev}"),
619        format!("refs/heads/{abbrev}"),
620        format!("refs/remotes/{abbrev}"),
621        format!("refs/remotes/{abbrev}/HEAD"),
622    ];
623    for (index, candidate) in expansions.iter().enumerate() {
624        if candidate == full {
625            return expansions.len() - index;
626        }
627    }
628    0
629}
630
631/// Whether `abbrev` (a possibly-abbreviated ref like `three` or `refs/heads/main`)
632/// matches the full ref `full` under git's `ref_rev_parse_rules` expansion, the
633/// way `refname_match`/`branch_merge_matches` (remote.c) compare a configured
634/// `branch.<name>.merge` value against an advertised ref name.
635pub fn refname_matches(abbrev: &str, full: &str) -> bool {
636    fetch_refname_match_score(abbrev, full) > 0
637}
638
639/// Qualify a fetch refspec destination the way upstream's `get_local_ref`
640/// (remote.c) does: `refs/...` stays as-is, `heads/`, `tags/` and `remotes/`
641/// gain a `refs/` prefix, and anything else lands under `refs/heads/`.
642fn fetch_local_ref_name(name: &str) -> String {
643    if name.starts_with("refs/") {
644        name.to_string()
645    } else if name.starts_with("heads/")
646        || name.starts_with("tags/")
647        || name.starts_with("remotes/")
648    {
649        format!("refs/{name}")
650    } else {
651        format!("refs/heads/{name}")
652    }
653}
654
655pub fn plan_fetch_ref_updates(
656    refs: &[RefAdvertisement],
657    refspecs: &[RefSpec],
658    auto_follow_tags: bool,
659) -> Result<Vec<FetchRefUpdate>> {
660    let negative = refspecs
661        .iter()
662        .filter(|refspec| refspec.negative)
663        .collect::<Vec<_>>();
664    let mut updates = Vec::new();
665    for refspec in refspecs.iter().filter(|refspec| !refspec.negative) {
666        validate_refspec_shape(refspec)?;
667        let Some(src) = refspec.src.as_deref() else {
668            return Err(GitError::InvalidFormat(
669                "fetch refspec is missing a source".into(),
670            ));
671        };
672        if refspec.pattern {
673            for reference in refs {
674                if refspec_is_excluded(&negative, &reference.name)? {
675                    continue;
676                }
677                if let Some(dst) = refspec_map_source(refspec, &reference.name)? {
678                    updates.push(FetchRefUpdate {
679                        src: reference.name.clone(),
680                        dst: Some(dst),
681                        oid: reference.oid,
682                        not_for_merge: false,
683                    });
684                }
685            }
686            continue;
687        }
688        if refspec_is_excluded(&negative, src)? {
689            continue;
690        }
691        let Some(reference) = find_advertised_ref_by_name_abbrev(refs, src) else {
692            return Err(GitError::reference_not_found(format!("remote ref {src}")));
693        };
694        updates.push(FetchRefUpdate {
695            src: reference.name.clone(),
696            dst: refspec.dst.as_deref().map(fetch_local_ref_name),
697            oid: reference.oid,
698            not_for_merge: false,
699        });
700    }
701    if auto_follow_tags && updates.iter().any(|update| update.dst.is_some()) {
702        let fetched_oids = updates.iter().map(|update| update.oid).collect::<Vec<_>>();
703        let fetched_srcs = updates
704            .iter()
705            .map(|update| update.src.clone())
706            .collect::<Vec<_>>();
707        for reference in refs {
708            if reference.name.starts_with("refs/tags/")
709                && fetched_oids.iter().any(|oid| oid == &reference.oid)
710                && !fetched_srcs.contains(&reference.name)
711                && !refspec_is_excluded(&negative, &reference.name)?
712            {
713                updates.push(FetchRefUpdate {
714                    src: reference.name.clone(),
715                    dst: Some(reference.name.clone()),
716                    oid: reference.oid,
717                    not_for_merge: true,
718                });
719            }
720        }
721    }
722    Ok(updates)
723}
724
725pub fn fetch_ref_updates_to_fetch_head(
726    updates: &[FetchRefUpdate],
727    remote: &str,
728) -> Result<Vec<FetchHeadRecord>> {
729    updates
730        .iter()
731        .map(|update| {
732            Ok(FetchHeadRecord {
733                oid: update.oid,
734                not_for_merge: update.not_for_merge,
735                description: fetch_head_remote_description(&update.src, remote)?,
736            })
737        })
738        .collect()
739}
740
741pub fn plan_push_commands(
742    format: ObjectFormat,
743    local_refs: &[PushSourceRef],
744    remote_refs: &[RefAdvertisement],
745    refspecs: &[RefSpec],
746) -> Result<Vec<ReceivePackCommand>> {
747    let zero = zero_object_id(format)?;
748    let mut commands = Vec::new();
749    for refspec in refspecs {
750        validate_refspec_shape(refspec)?;
751        if refspec.negative {
752            return Err(GitError::InvalidFormat(
753                "push refspec must not be negative".into(),
754            ));
755        }
756        match (refspec.src.as_deref(), refspec.dst.as_deref()) {
757            (None, None) => {
758                // A bare ":" (matching) refspec pushes only refs the remote
759                // already has, by their fully-qualified `refs/...` name. git's
760                // matching source set is the local ref advertisement, which never
761                // includes `HEAD` or short-name aliases — push those would try to
762                // update the remote's `HEAD`, so skip anything not under `refs/`.
763                for local in local_refs {
764                    if !local.name.starts_with("refs/") {
765                        continue;
766                    }
767                    validate_push_source_ref(format, local)?;
768                    if let Some(remote) = remote_ref(remote_refs, &local.name) {
769                        commands.push(ReceivePackCommand {
770                            old_id: remote.oid,
771                            new_id: local.oid,
772                            name: local.name.clone(),
773                        });
774                    }
775                }
776            }
777            (None, Some(dst)) => {
778                validate_refspec_endpoint("push destination", dst)?;
779                let remote = remote_ref(remote_refs, dst)
780                    .ok_or_else(|| GitError::reference_not_found(format!("remote ref {dst}")))?;
781                commands.push(ReceivePackCommand {
782                    old_id: remote.oid,
783                    new_id: zero.clone(),
784                    name: dst.to_string(),
785                });
786            }
787            (Some(src), dst) if refspec.pattern => {
788                let Some((src_prefix, src_suffix)) = src.split_once('*') else {
789                    return Err(GitError::InvalidFormat(
790                        "pattern push refspec source is missing wildcard".into(),
791                    ));
792                };
793                let dst = dst.ok_or_else(|| {
794                    GitError::InvalidFormat("pattern push refspec is missing destination".into())
795                })?;
796                let (dst_prefix, dst_suffix) = dst.split_once('*').ok_or_else(|| {
797                    GitError::InvalidFormat(
798                        "pattern push refspec destination is missing wildcard".into(),
799                    )
800                })?;
801                for local in local_refs {
802                    validate_push_source_ref(format, local)?;
803                    let Some(middle) = local
804                        .name
805                        .strip_prefix(src_prefix)
806                        .and_then(|value| value.strip_suffix(src_suffix))
807                    else {
808                        continue;
809                    };
810                    let name = format!("{dst_prefix}{middle}{dst_suffix}");
811                    let old_id = remote_ref(remote_refs, &name)
812                        .map(|reference| reference.oid)
813                        .unwrap_or_else(|| zero.clone());
814                    commands.push(ReceivePackCommand {
815                        old_id,
816                        new_id: local.oid,
817                        name,
818                    });
819                }
820            }
821            (Some(src), dst) => {
822                validate_refspec_endpoint("push source", src)?;
823                let local = local_ref(local_refs, src)
824                    .ok_or_else(|| GitError::reference_not_found(format!("local ref {src}")))?;
825                validate_push_source_ref(format, local)?;
826                let name = dst.unwrap_or(src);
827                validate_refspec_endpoint("push destination", name)?;
828                let old_id = remote_ref(remote_refs, name)
829                    .map(|reference| reference.oid)
830                    .unwrap_or_else(|| zero.clone());
831                commands.push(ReceivePackCommand {
832                    old_id,
833                    new_id: local.oid,
834                    name: name.to_string(),
835                });
836            }
837        }
838    }
839    Ok(commands)
840}
841
842pub fn build_receive_pack_push_request(
843    features: &ReceivePackFeatures,
844    commands: Vec<ReceivePackCommand>,
845    packfile: Vec<u8>,
846    options: ReceivePackPushRequestOptions,
847) -> Result<ReceivePackPushRequest> {
848    let mut capabilities = Vec::new();
849    if options.report_status_v2 {
850        require_receive_pack_feature(features.report_status_v2, "report-status-v2")?;
851        capabilities.push(Capability {
852            name: "report-status-v2".into(),
853            value: None,
854        });
855    } else if options.report_status {
856        require_receive_pack_feature(features.report_status, "report-status")?;
857        capabilities.push(Capability {
858            name: "report-status".into(),
859            value: None,
860        });
861    }
862    if commands.iter().any(is_receive_pack_delete_command) {
863        require_receive_pack_feature(features.delete_refs, "delete-refs")?;
864        capabilities.push(Capability {
865            name: "delete-refs".into(),
866            value: None,
867        });
868    }
869    if options.atomic {
870        require_receive_pack_feature(features.atomic, "atomic")?;
871        capabilities.push(Capability {
872            name: "atomic".into(),
873            value: None,
874        });
875    }
876    if options.ofs_delta {
877        require_receive_pack_feature(features.ofs_delta, "ofs-delta")?;
878        capabilities.push(Capability {
879            name: "ofs-delta".into(),
880            value: None,
881        });
882    }
883    if options.side_band_64k {
884        require_receive_pack_feature(features.side_band_64k, "side-band-64k")?;
885        capabilities.push(Capability {
886            name: "side-band-64k".into(),
887            value: None,
888        });
889    }
890    if options.quiet {
891        require_receive_pack_feature(features.quiet, "quiet")?;
892        capabilities.push(Capability {
893            name: "quiet".into(),
894            value: None,
895        });
896    }
897    if let Some(agent) = &options.agent {
898        validate_capability_field("receive-pack request agent", agent)?;
899        capabilities.push(Capability {
900            name: "agent".into(),
901            value: Some(agent.clone()),
902        });
903    }
904    if let Some(format) = options.object_format {
905        if features.object_format != Some(format) {
906            return Err(GitError::InvalidFormat(
907                "receive-pack request object-format was not advertised".into(),
908            ));
909        }
910        capabilities.push(Capability {
911            name: "object-format".into(),
912            value: Some(format.name().into()),
913        });
914    }
915    let push_options = if options.push_options.is_empty() {
916        None
917    } else {
918        require_receive_pack_feature(features.push_options, "push-options")?;
919        for option in &options.push_options {
920            validate_receive_pack_push_option(option.as_bytes())?;
921        }
922        capabilities.push(Capability {
923            name: "push-options".into(),
924            value: None,
925        });
926        Some(options.push_options)
927    };
928    let request = ReceivePackPushRequest {
929        commands: ReceivePackRequest {
930            commands,
931            capabilities,
932            shallow: Vec::new(),
933        },
934        push_options,
935        packfile,
936    };
937    validate_receive_pack_push_request_features(features, &request)?;
938    Ok(request)
939}
940
941pub fn smart_http_info_refs_path(repository_path: &str, service: GitService) -> Result<String> {
942    validate_smart_http_service(service)?;
943    let repository_path = normalize_http_repository_path(repository_path)?;
944    Ok(format!(
945        "{repository_path}/info/refs?service={}",
946        service.as_str()
947    ))
948}
949
950pub fn smart_http_rpc_path(repository_path: &str, service: GitService) -> Result<String> {
951    validate_smart_http_service(service)?;
952    let repository_path = normalize_http_repository_path(repository_path)?;
953    Ok(format!("{repository_path}/{}", service.as_str()))
954}
955
956pub fn dumb_http_info_refs_path(repository_path: &str) -> Result<String> {
957    let repository_path = normalize_http_repository_path(repository_path)?;
958    Ok(format!("{repository_path}/info/refs"))
959}
960
961pub fn dumb_http_alternates_path(repository_path: &str) -> Result<String> {
962    let repository_path = normalize_http_repository_path(repository_path)?;
963    Ok(format!("{repository_path}/objects/info/http-alternates"))
964}
965
966pub fn dumb_http_packs_path(repository_path: &str) -> Result<String> {
967    let repository_path = normalize_http_repository_path(repository_path)?;
968    Ok(format!("{repository_path}/objects/info/packs"))
969}
970
971pub fn dumb_http_loose_object_path(repository_path: &str, oid: &ObjectId) -> Result<String> {
972    let repository_path = normalize_http_repository_path(repository_path)?;
973    let oid = oid.to_string();
974    let (directory, file) = oid.split_at(2);
975    Ok(format!("{repository_path}/objects/{directory}/{file}"))
976}
977
978pub fn dumb_http_pack_file_path(repository_path: &str, hash: &ObjectId) -> Result<String> {
979    dumb_http_pack_resource_path(repository_path, hash, "pack")
980}
981
982pub fn dumb_http_pack_index_path(repository_path: &str, hash: &ObjectId) -> Result<String> {
983    dumb_http_pack_resource_path(repository_path, hash, "idx")
984}
985
986pub fn smart_http_advertisement_content_type(service: GitService) -> Result<String> {
987    validate_smart_http_service(service)?;
988    Ok(format!("application/x-{}-advertisement", service.as_str()))
989}
990
991pub fn smart_http_rpc_request_content_type(service: GitService) -> Result<String> {
992    validate_smart_http_service(service)?;
993    Ok(format!("application/x-{}-request", service.as_str()))
994}
995
996pub fn smart_http_rpc_result_content_type(service: GitService) -> Result<String> {
997    validate_smart_http_service(service)?;
998    Ok(format!("application/x-{}-result", service.as_str()))
999}
1000
1001pub fn parse_smart_http_advertisement_content_type(value: &str) -> Result<GitService> {
1002    parse_smart_http_content_type(value, "-advertisement")
1003}
1004
1005pub fn parse_smart_http_rpc_request_content_type(value: &str) -> Result<GitService> {
1006    parse_smart_http_content_type(value, "-request")
1007}
1008
1009pub fn parse_smart_http_rpc_result_content_type(value: &str) -> Result<GitService> {
1010    parse_smart_http_content_type(value, "-result")
1011}
1012
1013pub fn parse_sideband_packet(payload: &[u8]) -> Result<SideBandPacket> {
1014    let Some((&channel, data)) = payload.split_first() else {
1015        return Err(GitError::InvalidFormat("sideband packet is empty".into()));
1016    };
1017    let channel = match channel {
1018        1 => SideBandChannel::Data,
1019        2 => SideBandChannel::Progress,
1020        3 => SideBandChannel::Fatal,
1021        other => {
1022            return Err(GitError::InvalidFormat(format!(
1023                "invalid sideband channel {other}"
1024            )));
1025        }
1026    };
1027    Ok(SideBandPacket {
1028        channel,
1029        data: data.to_vec(),
1030    })
1031}
1032
1033pub fn encode_sideband_packet(packet: &SideBandPacket) -> Result<Vec<u8>> {
1034    let mut out = Vec::with_capacity(packet.data.len() + 1);
1035    out.push(match packet.channel {
1036        SideBandChannel::Data => 1,
1037        SideBandChannel::Progress => 2,
1038        SideBandChannel::Fatal => 3,
1039    });
1040    out.extend_from_slice(&packet.data);
1041    if out.len() > PKT_LINE_MAX_PAYLOAD_LEN {
1042        return Err(GitError::InvalidFormat(format!(
1043            "sideband packet exceeds {PKT_LINE_MAX_PAYLOAD_LEN} bytes"
1044        )));
1045    }
1046    Ok(out)
1047}
1048
1049pub fn write_sideband_packet(writer: &mut impl Write, packet: &SideBandPacket) -> Result<()> {
1050    write_sideband_payload(writer, packet.channel, &packet.data)
1051}
1052
1053fn write_sideband_payload(
1054    writer: &mut impl Write,
1055    channel: SideBandChannel,
1056    data: &[u8],
1057) -> Result<()> {
1058    let payload_len = data
1059        .len()
1060        .checked_add(1)
1061        .ok_or_else(|| GitError::InvalidFormat("sideband packet length overflow".into()))?;
1062    if payload_len > PKT_LINE_MAX_PAYLOAD_LEN {
1063        return Err(GitError::InvalidFormat(format!(
1064            "sideband packet exceeds {PKT_LINE_MAX_PAYLOAD_LEN} bytes"
1065        )));
1066    }
1067    writer.write_all(&pkt_line_header(payload_len + 4))?;
1068    writer.write_all(&[match channel {
1069        SideBandChannel::Data => 1,
1070        SideBandChannel::Progress => 2,
1071        SideBandChannel::Fatal => 3,
1072    }])?;
1073    writer.write_all(data)?;
1074    Ok(())
1075}
1076
1077pub fn parse_sideband_packets(payloads: &[Vec<u8>]) -> Result<Vec<SideBandPacket>> {
1078    payloads
1079        .iter()
1080        .map(|payload| parse_sideband_packet(payload))
1081        .collect()
1082}
1083
1084pub fn encode_sideband_packets(packets: &[SideBandPacket]) -> Result<Vec<Vec<u8>>> {
1085    packets.iter().map(encode_sideband_packet).collect()
1086}
1087
1088pub fn parse_sideband_stream(frames: &[PktLineFrame]) -> Result<Vec<SideBandPacket>> {
1089    let mut packets = Vec::new();
1090    let mut saw_flush = false;
1091    for (idx, frame) in frames.iter().enumerate() {
1092        match frame {
1093            PktLineFrame::Data(payload) if !saw_flush => {
1094                packets.push(parse_sideband_packet(payload)?);
1095            }
1096            PktLineFrame::Data(_) => {
1097                return Err(GitError::InvalidFormat(
1098                    "sideband stream has data after flush".into(),
1099                ));
1100            }
1101            PktLineFrame::Flush => {
1102                saw_flush = true;
1103                if idx + 1 != frames.len() {
1104                    return Err(GitError::InvalidFormat(
1105                        "sideband stream has frames after flush".into(),
1106                    ));
1107                }
1108            }
1109            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
1110                return Err(GitError::InvalidFormat(
1111                    "sideband stream contains a non-flush control packet".into(),
1112                ));
1113            }
1114        }
1115    }
1116    if !saw_flush {
1117        return Err(GitError::InvalidFormat(
1118            "sideband stream missing flush".into(),
1119        ));
1120    }
1121    Ok(packets)
1122}
1123
1124pub fn encode_sideband_stream(packets: &[SideBandPacket]) -> Result<Vec<PktLineFrame>> {
1125    let mut frames = Vec::new();
1126    for packet in packets {
1127        frames.push(PktLineFrame::data(encode_sideband_packet(packet)?)?);
1128    }
1129    frames.push(PktLineFrame::Flush);
1130    Ok(frames)
1131}
1132
1133pub fn read_sideband_stream(reader: &mut impl Read) -> Result<Vec<SideBandPacket>> {
1134    let frames = read_pkt_line_frames_until_flush(reader)?;
1135    parse_sideband_stream(&frames)
1136}
1137
1138pub fn write_sideband_stream(writer: &mut impl Write, packets: &[SideBandPacket]) -> Result<()> {
1139    for packet in packets {
1140        write_sideband_packet(writer, packet)?;
1141    }
1142    writer.write_all(b"0000")?;
1143    Ok(())
1144}
1145
1146pub fn demux_sideband_packets(packets: &[SideBandPacket]) -> Result<SideBandDemux> {
1147    let mut out = SideBandDemux::default();
1148    for packet in packets {
1149        match packet.channel {
1150            SideBandChannel::Data => out.data.extend_from_slice(&packet.data),
1151            SideBandChannel::Progress => out.progress.push(packet.data.clone()),
1152            SideBandChannel::Fatal => {
1153                let message = String::from_utf8_lossy(&packet.data).into_owned();
1154                return Err(GitError::InvalidFormat(format!(
1155                    "sideband fatal: {message}"
1156                )));
1157            }
1158        }
1159    }
1160    Ok(out)
1161}
1162
1163pub fn parse_and_demux_sideband_packets(payloads: &[Vec<u8>]) -> Result<SideBandDemux> {
1164    let packets = parse_sideband_packets(payloads)?;
1165    demux_sideband_packets(&packets)
1166}
1167
1168pub fn demux_sideband_stream(frames: &[PktLineFrame]) -> Result<SideBandDemux> {
1169    let packets = parse_sideband_stream(frames)?;
1170    demux_sideband_packets(&packets)
1171}
1172
1173pub fn read_and_demux_sideband_stream(reader: &mut impl Read) -> Result<SideBandDemux> {
1174    let packets = read_sideband_stream(reader)?;
1175    demux_sideband_packets(&packets)
1176}
1177
1178pub fn parse_upload_archive_request(frames: &[PktLineFrame]) -> Result<UploadArchiveRequest> {
1179    let mut request = UploadArchiveRequest::default();
1180    let mut saw_flush = false;
1181    for (idx, frame) in frames.iter().enumerate() {
1182        match frame {
1183            PktLineFrame::Data(payload) if !saw_flush => {
1184                let text = parse_protocol_v2_line_text("upload-archive request argument", payload)?;
1185                let argument = text.strip_prefix("argument ").ok_or_else(|| {
1186                    GitError::InvalidFormat("upload-archive request line must be argument".into())
1187                })?;
1188                validate_upload_archive_argument(argument)?;
1189                request.arguments.push(argument.to_string());
1190            }
1191            PktLineFrame::Data(_) => {
1192                return Err(GitError::InvalidFormat(
1193                    "upload-archive request has data after flush".into(),
1194                ));
1195            }
1196            PktLineFrame::Flush => {
1197                saw_flush = true;
1198                if idx + 1 != frames.len() {
1199                    return Err(GitError::InvalidFormat(
1200                        "upload-archive request has frames after flush".into(),
1201                    ));
1202                }
1203            }
1204            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
1205                return Err(GitError::InvalidFormat(
1206                    "upload-archive request contains a non-flush control packet".into(),
1207                ));
1208            }
1209        }
1210    }
1211    if !saw_flush {
1212        return Err(GitError::InvalidFormat(
1213            "upload-archive request missing flush".into(),
1214        ));
1215    }
1216    if request.arguments.is_empty() {
1217        return Err(GitError::InvalidFormat(
1218            "upload-archive request is missing arguments".into(),
1219        ));
1220    }
1221    Ok(request)
1222}
1223
1224pub fn encode_upload_archive_request(request: &UploadArchiveRequest) -> Result<Vec<PktLineFrame>> {
1225    if request.arguments.is_empty() {
1226        return Err(GitError::InvalidFormat(
1227            "upload-archive request is missing arguments".into(),
1228        ));
1229    }
1230    let mut frames = Vec::new();
1231    for argument in &request.arguments {
1232        validate_upload_archive_argument(argument)?;
1233        frames.push(PktLineFrame::data(line_from_str(&format!(
1234            "argument {argument}"
1235        )))?);
1236    }
1237    frames.push(PktLineFrame::Flush);
1238    Ok(frames)
1239}
1240
1241pub fn read_upload_archive_request(reader: &mut impl Read) -> Result<UploadArchiveRequest> {
1242    let frames = read_pkt_line_frames_until_flush(reader)?;
1243    parse_upload_archive_request(&frames)
1244}
1245
1246pub fn write_upload_archive_request(
1247    writer: &mut impl Write,
1248    request: &UploadArchiveRequest,
1249) -> Result<()> {
1250    if request.arguments.is_empty() {
1251        return Err(GitError::InvalidFormat(
1252            "upload-archive request is missing arguments".into(),
1253        ));
1254    }
1255    for argument in &request.arguments {
1256        validate_upload_archive_argument(argument)?;
1257        write_pkt_line_payload(writer, &line_from_str(&format!("argument {argument}")))?;
1258    }
1259    writer.write_all(b"0000")?;
1260    Ok(())
1261}
1262
1263pub fn parse_upload_archive_response(frames: &[PktLineFrame]) -> Result<UploadArchiveResponse> {
1264    let Some((first, rest)) = frames.split_first() else {
1265        return Err(GitError::InvalidFormat(
1266            "upload-archive response is empty".into(),
1267        ));
1268    };
1269    let PktLineFrame::Data(payload) = first else {
1270        return Err(GitError::InvalidFormat(
1271            "upload-archive response must start with a data packet".into(),
1272        ));
1273    };
1274    let text = parse_protocol_v2_line_text("upload-archive response status", payload)?;
1275    if text == "ACK" {
1276        return Ok(UploadArchiveResponse::Ack {
1277            sideband: parse_sideband_stream(rest)?,
1278        });
1279    }
1280    if let Some(message) = text.strip_prefix("NACK ") {
1281        validate_upload_archive_status_message(message)?;
1282        if !matches!(rest, [PktLineFrame::Flush]) {
1283            return Err(GitError::InvalidFormat(
1284                "upload-archive NACK response must end with flush".into(),
1285            ));
1286        }
1287        return Ok(UploadArchiveResponse::Nack {
1288            message: message.to_string(),
1289        });
1290    }
1291    Err(GitError::InvalidFormat(format!(
1292        "unsupported upload-archive response status {text}"
1293    )))
1294}
1295
1296pub fn encode_upload_archive_response(
1297    response: &UploadArchiveResponse,
1298) -> Result<Vec<PktLineFrame>> {
1299    let mut frames = Vec::new();
1300    match response {
1301        UploadArchiveResponse::Ack { sideband } => {
1302            frames.push(PktLineFrame::data(line_from_str("ACK"))?);
1303            frames.extend(encode_sideband_stream(sideband)?);
1304        }
1305        UploadArchiveResponse::Nack { message } => {
1306            validate_upload_archive_status_message(message)?;
1307            frames.push(PktLineFrame::data(line_from_str(&format!(
1308                "NACK {message}"
1309            )))?);
1310            frames.push(PktLineFrame::Flush);
1311        }
1312    }
1313    Ok(frames)
1314}
1315
1316pub fn read_upload_archive_response(reader: &mut impl Read) -> Result<UploadArchiveResponse> {
1317    let frames = read_pkt_line_frames_until_flush(reader)?;
1318    parse_upload_archive_response(&frames)
1319}
1320
1321pub fn write_upload_archive_response(
1322    writer: &mut impl Write,
1323    response: &UploadArchiveResponse,
1324) -> Result<()> {
1325    match response {
1326        UploadArchiveResponse::Ack { sideband } => {
1327            write_pkt_line_payload(writer, b"ACK\n")?;
1328            write_sideband_stream(writer, sideband)?;
1329        }
1330        UploadArchiveResponse::Nack { message } => {
1331            validate_upload_archive_status_message(message)?;
1332            write_pkt_line_payload(writer, &line_from_str(&format!("NACK {message}")))?;
1333            writer.write_all(b"0000")?;
1334        }
1335    }
1336    Ok(())
1337}
1338
1339pub fn demux_upload_archive_response(response: &UploadArchiveResponse) -> Result<SideBandDemux> {
1340    match response {
1341        UploadArchiveResponse::Ack { sideband } => demux_sideband_packets(sideband),
1342        UploadArchiveResponse::Nack { message } => Err(GitError::InvalidFormat(format!(
1343            "upload-archive NACK: {message}"
1344        ))),
1345    }
1346}
1347
1348fn parse_pkt_len(bytes: &[u8]) -> Result<usize> {
1349    let mut len = 0usize;
1350    for byte in bytes {
1351        len = (len << 4) | hex_nibble(*byte)? as usize;
1352    }
1353    Ok(len)
1354}
1355
1356fn hex_nibble(byte: u8) -> Result<u8> {
1357    match byte {
1358        b'0'..=b'9' => Ok(byte - b'0'),
1359        b'a'..=b'f' => Ok(byte - b'a' + 10),
1360        b'A'..=b'F' => Ok(byte - b'A' + 10),
1361        _ => Err(GitError::InvalidFormat(format!(
1362            "invalid pkt-line length byte {byte:#04x}"
1363        ))),
1364    }
1365}
1366
1367#[derive(Debug, Clone, PartialEq, Eq)]
1368pub struct TransportHandshake {
1369    pub protocol: ProtocolVersion,
1370    pub capabilities: Vec<Capability>,
1371}
1372
1373#[derive(Debug, Clone, PartialEq, Eq)]
1374pub struct RefAdvertisement {
1375    pub oid: ObjectId,
1376    pub name: String,
1377    pub capabilities: Vec<Capability>,
1378}
1379
1380#[derive(Debug, Clone, PartialEq, Eq)]
1381pub struct DumbHttpRefRecord {
1382    pub oid: ObjectId,
1383    pub name: String,
1384    pub peeled: bool,
1385}
1386
1387#[derive(Debug, Clone, PartialEq, Eq)]
1388pub struct DumbHttpPackRecord {
1389    pub hash: ObjectId,
1390}
1391
1392#[derive(Debug, Clone, PartialEq, Eq)]
1393pub struct RefAdvertisementSet {
1394    pub protocol: ProtocolVersion,
1395    pub refs: Vec<RefAdvertisement>,
1396    pub shallow: Vec<ObjectId>,
1397}
1398
1399#[derive(Debug, Clone, PartialEq, Eq, Default)]
1400pub struct UploadPackRequest {
1401    pub wants: Vec<ObjectId>,
1402    pub capabilities: Vec<Capability>,
1403    pub shallow: Vec<ObjectId>,
1404    pub deepen: Option<u32>,
1405    pub deepen_since: Option<u64>,
1406    pub deepen_not: Vec<String>,
1407    pub filter: Option<String>,
1408}
1409
1410#[derive(Debug, Clone, PartialEq, Eq, Default)]
1411pub struct UploadPackFeatures {
1412    pub multi_ack: bool,
1413    pub multi_ack_detailed: bool,
1414    pub no_done: bool,
1415    pub thin_pack: bool,
1416    pub side_band: bool,
1417    pub side_band_64k: bool,
1418    pub ofs_delta: bool,
1419    pub shallow: bool,
1420    pub deepen_since: bool,
1421    pub deepen_not: bool,
1422    pub include_tag: bool,
1423    pub no_progress: bool,
1424    pub allow_tip_sha1_in_want: bool,
1425    pub allow_reachable_sha1_in_want: bool,
1426    pub filter: bool,
1427    pub agent: Option<String>,
1428    pub object_format: Option<ObjectFormat>,
1429    pub symrefs: Vec<String>,
1430    pub unknown: Vec<Capability>,
1431}
1432
1433#[derive(Debug, Clone, PartialEq, Eq, Default)]
1434pub struct UploadPackNegotiationRequest {
1435    pub haves: Vec<ObjectId>,
1436    pub done: bool,
1437}
1438
1439#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1440pub enum UploadPackAckStatus {
1441    Continue,
1442    Common,
1443    Ready,
1444}
1445
1446#[derive(Debug, Clone, PartialEq, Eq)]
1447pub enum UploadPackAcknowledgment {
1448    Nak,
1449    Ack {
1450        oid: ObjectId,
1451        status: Option<UploadPackAckStatus>,
1452    },
1453}
1454
1455#[derive(Debug, Clone, PartialEq, Eq, Default)]
1456pub struct UploadPackPackfileResponse {
1457    pub acknowledgments: Vec<UploadPackAcknowledgment>,
1458    pub sideband: Vec<SideBandPacket>,
1459}
1460
1461#[derive(Debug, Clone, PartialEq, Eq, Default)]
1462pub struct UploadPackRawPackfileResponse {
1463    pub acknowledgments: Vec<UploadPackAcknowledgment>,
1464    pub packfile: Vec<u8>,
1465}
1466
1467#[derive(Debug, Clone, PartialEq, Eq)]
1468pub struct ReceivePackCommand {
1469    pub old_id: ObjectId,
1470    pub new_id: ObjectId,
1471    pub name: String,
1472}
1473
1474#[derive(Debug, Clone, PartialEq, Eq, Default)]
1475pub struct ReceivePackRequest {
1476    pub shallow: Vec<ObjectId>,
1477    pub commands: Vec<ReceivePackCommand>,
1478    pub capabilities: Vec<Capability>,
1479}
1480
1481#[derive(Debug, Clone, PartialEq, Eq, Default)]
1482pub struct ReceivePackPushRequest {
1483    pub commands: ReceivePackRequest,
1484    pub push_options: Option<Vec<String>>,
1485    pub packfile: Vec<u8>,
1486}
1487
1488#[derive(Debug, Clone, PartialEq, Eq, Default)]
1489pub struct ReceivePackPushRequestOptions {
1490    pub report_status: bool,
1491    pub report_status_v2: bool,
1492    pub atomic: bool,
1493    pub ofs_delta: bool,
1494    pub side_band_64k: bool,
1495    pub quiet: bool,
1496    pub agent: Option<String>,
1497    pub object_format: Option<ObjectFormat>,
1498    pub push_options: Vec<String>,
1499}
1500
1501#[derive(Debug, Clone, PartialEq, Eq, Default)]
1502pub struct ReceivePackFeatures {
1503    pub report_status: bool,
1504    pub report_status_v2: bool,
1505    pub delete_refs: bool,
1506    pub ofs_delta: bool,
1507    pub atomic: bool,
1508    pub push_options: bool,
1509    pub side_band_64k: bool,
1510    pub quiet: bool,
1511    pub no_thin: bool,
1512    pub agent: Option<String>,
1513    pub object_format: Option<ObjectFormat>,
1514    pub unknown: Vec<Capability>,
1515}
1516
1517#[derive(Debug, Clone, PartialEq, Eq)]
1518pub enum ReceivePackUnpackStatus {
1519    Ok,
1520    Error(String),
1521}
1522
1523#[derive(Debug, Clone, PartialEq, Eq)]
1524pub enum ReceivePackCommandStatus {
1525    Ok { name: String },
1526    Ng { name: String, message: String },
1527}
1528
1529#[derive(Debug, Clone, PartialEq, Eq)]
1530pub struct ReceivePackReportStatus {
1531    pub unpack: ReceivePackUnpackStatus,
1532    pub commands: Vec<ReceivePackCommandStatus>,
1533}
1534
1535#[derive(Debug, Clone, PartialEq, Eq, Default)]
1536pub struct ReceivePackCommandStatusV2Options {
1537    pub refname: Option<String>,
1538    pub old_oid: Option<ObjectId>,
1539    pub new_oid: Option<ObjectId>,
1540    pub forced_update: bool,
1541}
1542
1543#[derive(Debug, Clone, PartialEq, Eq)]
1544pub enum ReceivePackCommandStatusV2 {
1545    Ok {
1546        name: String,
1547        options: ReceivePackCommandStatusV2Options,
1548    },
1549    Ng {
1550        name: String,
1551        message: String,
1552    },
1553}
1554
1555#[derive(Debug, Clone, PartialEq, Eq)]
1556pub struct ReceivePackReportStatusV2 {
1557    pub unpack: ReceivePackUnpackStatus,
1558    pub commands: Vec<ReceivePackCommandStatusV2>,
1559}
1560
1561#[derive(Debug, Clone, PartialEq, Eq)]
1562pub struct ProtocolV2CommandRequest {
1563    pub command: String,
1564    pub capabilities: Vec<Capability>,
1565    pub arguments: Vec<Vec<u8>>,
1566}
1567
1568#[derive(Debug, Clone, PartialEq, Eq)]
1569pub enum ProtocolV2Request {
1570    Command(ProtocolV2CommandRequest),
1571    Done,
1572}
1573
1574#[derive(Debug, Clone, PartialEq, Eq)]
1575pub enum ProtocolV2Command {
1576    LsRefs(ProtocolV2LsRefsRequest),
1577    Fetch(ProtocolV2FetchRequest),
1578    ObjectInfo(ProtocolV2ObjectInfoRequest),
1579    Unknown(ProtocolV2CommandRequest),
1580}
1581
1582#[derive(Debug, Clone, PartialEq, Eq)]
1583pub enum ProtocolV2SessionRequest {
1584    Command(ProtocolV2Command),
1585    Done,
1586}
1587
1588#[derive(Debug, Clone, PartialEq, Eq, Default)]
1589pub struct ProtocolV2CommandOptions {
1590    pub agent: Option<String>,
1591    pub object_format: Option<ObjectFormat>,
1592    pub server_options: Vec<String>,
1593    pub extra: Vec<Capability>,
1594}
1595
1596#[derive(Debug, Clone, PartialEq, Eq, Default)]
1597pub struct ProtocolV2FetchFeatures {
1598    pub shallow: bool,
1599    pub wait_for_done: bool,
1600    pub filter: bool,
1601    pub ref_in_want: bool,
1602    pub sideband_all: bool,
1603    pub packfile_uris: bool,
1604    pub unknown: Vec<String>,
1605}
1606
1607#[derive(Debug, Clone, PartialEq, Eq, Default)]
1608pub struct ProtocolV2LsRefsFeatures {
1609    pub unborn: bool,
1610    pub unknown: Vec<String>,
1611}
1612
1613impl ProtocolV2CommandRequest {
1614    pub fn new(command: impl Into<String>) -> Result<Self> {
1615        let command = command.into();
1616        validate_capability_name(&command)?;
1617        Ok(Self {
1618            command,
1619            capabilities: Vec::new(),
1620            arguments: Vec::new(),
1621        })
1622    }
1623}
1624
1625#[derive(Debug, Clone, PartialEq, Eq, Default)]
1626pub struct ProtocolV2LsRefsRequest {
1627    pub peel: bool,
1628    pub symrefs: bool,
1629    pub unborn: bool,
1630    pub ref_prefixes: Vec<String>,
1631}
1632
1633#[derive(Debug, Clone, PartialEq, Eq)]
1634pub struct ProtocolV2LsRefsRef {
1635    pub oid: ObjectId,
1636    pub name: String,
1637    pub peeled: Option<ObjectId>,
1638    pub symref_target: Option<String>,
1639    pub attributes: Vec<String>,
1640}
1641
1642#[derive(Debug, Clone, PartialEq, Eq)]
1643pub enum ProtocolV2LsRefsRecord {
1644    Ref(ProtocolV2LsRefsRef),
1645    Unborn {
1646        name: String,
1647        symref_target: Option<String>,
1648        attributes: Vec<String>,
1649    },
1650}
1651
1652#[derive(Debug, Clone, PartialEq, Eq, Default)]
1653pub struct ProtocolV2FetchRequest {
1654    pub wants: Vec<ObjectId>,
1655    pub want_refs: Vec<String>,
1656    pub haves: Vec<ObjectId>,
1657    pub shallow: Vec<ObjectId>,
1658    pub deepen: Option<u32>,
1659    pub deepen_since: Option<u64>,
1660    pub deepen_not: Vec<String>,
1661    pub deepen_relative: bool,
1662    pub filter: Option<String>,
1663    pub packfile_uris: Option<String>,
1664    pub thin_pack: bool,
1665    pub no_progress: bool,
1666    pub include_tag: bool,
1667    pub ofs_delta: bool,
1668    pub sideband_all: bool,
1669    pub wait_for_done: bool,
1670    pub done: bool,
1671}
1672
1673#[derive(Debug, Clone, PartialEq, Eq)]
1674pub enum ProtocolV2FetchAcknowledgment {
1675    Nak,
1676    Ack(ObjectId),
1677    Ready,
1678}
1679
1680#[derive(Debug, Clone, PartialEq, Eq)]
1681pub enum ProtocolV2FetchShallowInfo {
1682    Shallow(ObjectId),
1683    Unshallow(ObjectId),
1684}
1685
1686#[derive(Debug, Clone, PartialEq, Eq)]
1687pub struct ProtocolV2FetchWantedRef {
1688    pub oid: ObjectId,
1689    pub name: String,
1690}
1691
1692#[derive(Debug, Clone, PartialEq, Eq)]
1693pub struct ProtocolV2FetchPackfileUri {
1694    pub pack_hash: ObjectId,
1695    pub uri: String,
1696}
1697
1698#[derive(Debug, Clone, PartialEq, Eq)]
1699pub enum ProtocolV2FetchResponseSection {
1700    Acknowledgments(Vec<ProtocolV2FetchAcknowledgment>),
1701    ShallowInfo(Vec<ProtocolV2FetchShallowInfo>),
1702    WantedRefs(Vec<ProtocolV2FetchWantedRef>),
1703    PackfileUris(Vec<ProtocolV2FetchPackfileUri>),
1704    Packfile(Vec<Vec<u8>>),
1705    Unknown { name: String, lines: Vec<Vec<u8>> },
1706}
1707
1708#[derive(Debug, Clone, PartialEq, Eq, Default)]
1709pub struct ProtocolV2FetchSidebandAllResponse {
1710    pub sections: Vec<ProtocolV2FetchResponseSection>,
1711    pub progress: Vec<Vec<u8>>,
1712}
1713
1714#[derive(Debug, Clone, PartialEq, Eq, Default)]
1715pub struct ProtocolV2ObjectInfoRequest {
1716    pub size: bool,
1717    pub oids: Vec<ObjectId>,
1718}
1719
1720#[derive(Debug, Clone, PartialEq, Eq)]
1721pub struct ProtocolV2ObjectInfoRecord {
1722    pub oid: ObjectId,
1723    pub size: u64,
1724}
1725
1726#[derive(Debug, Clone, PartialEq, Eq, Default)]
1727pub struct ProtocolV2ObjectInfoResponse {
1728    pub size: bool,
1729    pub records: Vec<ProtocolV2ObjectInfoRecord>,
1730}
1731
1732impl ProtocolV2LsRefsRequest {
1733    pub fn from_command_request(request: &ProtocolV2CommandRequest) -> Result<Self> {
1734        if request.command != "ls-refs" {
1735            return Err(GitError::InvalidFormat(format!(
1736                "expected ls-refs command, got {}",
1737                request.command
1738            )));
1739        }
1740        let mut out = Self::default();
1741        for argument in &request.arguments {
1742            let text = std::str::from_utf8(argument)
1743                .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
1744            match text {
1745                "peel" => out.peel = true,
1746                "symrefs" => out.symrefs = true,
1747                "unborn" => out.unborn = true,
1748                value if value.starts_with("ref-prefix ") => {
1749                    let prefix = value
1750                        .strip_prefix("ref-prefix ")
1751                        .ok_or_else(|| GitError::InvalidFormat("invalid ref-prefix".into()))?;
1752                    validate_protocol_v2_token("ls-refs ref-prefix", prefix)?;
1753                    out.ref_prefixes.push(prefix.to_string());
1754                }
1755                other => {
1756                    return Err(GitError::InvalidFormat(format!(
1757                        "unsupported ls-refs argument {other}"
1758                    )));
1759                }
1760            }
1761        }
1762        Ok(out)
1763    }
1764
1765    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
1766        let mut request = ProtocolV2CommandRequest::new("ls-refs")?;
1767        if self.peel {
1768            request.arguments.push(b"peel".to_vec());
1769        }
1770        if self.symrefs {
1771            request.arguments.push(b"symrefs".to_vec());
1772        }
1773        if self.unborn {
1774            request.arguments.push(b"unborn".to_vec());
1775        }
1776        for prefix in &self.ref_prefixes {
1777            validate_protocol_v2_token("ls-refs ref-prefix", prefix)?;
1778            request
1779                .arguments
1780                .push(format!("ref-prefix {prefix}").into_bytes());
1781        }
1782        Ok(request)
1783    }
1784}
1785
1786impl ProtocolV2FetchRequest {
1787    pub fn from_command_request(
1788        format: ObjectFormat,
1789        request: &ProtocolV2CommandRequest,
1790    ) -> Result<Self> {
1791        if request.command != "fetch" {
1792            return Err(GitError::InvalidFormat(format!(
1793                "expected fetch command, got {}",
1794                request.command
1795            )));
1796        }
1797        let mut out = Self::default();
1798        for argument in &request.arguments {
1799            let text = std::str::from_utf8(argument)
1800                .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
1801            match text {
1802                "thin-pack" => out.thin_pack = true,
1803                "no-progress" => out.no_progress = true,
1804                "include-tag" => out.include_tag = true,
1805                "ofs-delta" => out.ofs_delta = true,
1806                "sideband-all" => out.sideband_all = true,
1807                "wait-for-done" => out.wait_for_done = true,
1808                "deepen-relative" => out.deepen_relative = true,
1809                "done" => out.done = true,
1810                value if value.starts_with("want ") => {
1811                    out.wants
1812                        .push(parse_oid_argument(format, "fetch want", value, "want ")?);
1813                }
1814                value if value.starts_with("want-ref ") => {
1815                    let name = value
1816                        .strip_prefix("want-ref ")
1817                        .ok_or_else(|| GitError::InvalidFormat("invalid fetch want-ref".into()))?;
1818                    validate_protocol_v2_token("fetch want-ref", name)?;
1819                    out.want_refs.push(name.to_string());
1820                }
1821                value if value.starts_with("have ") => {
1822                    out.haves
1823                        .push(parse_oid_argument(format, "fetch have", value, "have ")?);
1824                }
1825                value if value.starts_with("shallow ") => {
1826                    out.shallow.push(parse_oid_argument(
1827                        format,
1828                        "fetch shallow",
1829                        value,
1830                        "shallow ",
1831                    )?);
1832                }
1833                value if value.starts_with("deepen ") => {
1834                    if out.deepen.is_some() {
1835                        return Err(GitError::InvalidFormat(
1836                            "fetch request has duplicate deepen".into(),
1837                        ));
1838                    }
1839                    out.deepen = Some(parse_u32_argument("fetch deepen", value, "deepen ")?);
1840                }
1841                value if value.starts_with("deepen-since ") => {
1842                    if out.deepen_since.is_some() {
1843                        return Err(GitError::InvalidFormat(
1844                            "fetch request has duplicate deepen-since".into(),
1845                        ));
1846                    }
1847                    out.deepen_since = Some(parse_u64_argument(
1848                        "fetch deepen-since",
1849                        value,
1850                        "deepen-since ",
1851                    )?);
1852                }
1853                value if value.starts_with("deepen-not ") => {
1854                    let name = value.strip_prefix("deepen-not ").ok_or_else(|| {
1855                        GitError::InvalidFormat("invalid fetch deepen-not".into())
1856                    })?;
1857                    validate_protocol_v2_token("fetch deepen-not", name)?;
1858                    out.deepen_not.push(name.to_string());
1859                }
1860                value if value.starts_with("filter ") => {
1861                    if out.filter.is_some() {
1862                        return Err(GitError::InvalidFormat(
1863                            "fetch request has duplicate filter".into(),
1864                        ));
1865                    }
1866                    let filter = value
1867                        .strip_prefix("filter ")
1868                        .ok_or_else(|| GitError::InvalidFormat("invalid fetch filter".into()))?;
1869                    validate_protocol_v2_token("fetch filter", filter)?;
1870                    out.filter = Some(filter.to_string());
1871                }
1872                value if value.starts_with("packfile-uris ") => {
1873                    if out.packfile_uris.is_some() {
1874                        return Err(GitError::InvalidFormat(
1875                            "fetch request has duplicate packfile-uris".into(),
1876                        ));
1877                    }
1878                    let protocols = value.strip_prefix("packfile-uris ").ok_or_else(|| {
1879                        GitError::InvalidFormat("invalid fetch packfile-uris".into())
1880                    })?;
1881                    validate_protocol_v2_token("fetch packfile-uris", protocols)?;
1882                    out.packfile_uris = Some(protocols.to_string());
1883                }
1884                other => {
1885                    return Err(GitError::InvalidFormat(format!(
1886                        "unsupported fetch argument {other}"
1887                    )));
1888                }
1889            }
1890        }
1891        Ok(out)
1892    }
1893
1894    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
1895        let mut request = ProtocolV2CommandRequest::new("fetch")?;
1896        for oid in &self.wants {
1897            request.arguments.push(format!("want {oid}").into_bytes());
1898        }
1899        for name in &self.want_refs {
1900            validate_protocol_v2_token("fetch want-ref", name)?;
1901            request
1902                .arguments
1903                .push(format!("want-ref {name}").into_bytes());
1904        }
1905        for oid in &self.haves {
1906            request.arguments.push(format!("have {oid}").into_bytes());
1907        }
1908        for oid in &self.shallow {
1909            request
1910                .arguments
1911                .push(format!("shallow {oid}").into_bytes());
1912        }
1913        if let Some(deepen) = self.deepen {
1914            if deepen == 0 {
1915                return Err(GitError::InvalidFormat(
1916                    "fetch deepen must be positive".into(),
1917                ));
1918            }
1919            request
1920                .arguments
1921                .push(format!("deepen {deepen}").into_bytes());
1922        }
1923        if let Some(deepen_since) = self.deepen_since {
1924            request
1925                .arguments
1926                .push(format!("deepen-since {deepen_since}").into_bytes());
1927        }
1928        for name in &self.deepen_not {
1929            validate_protocol_v2_token("fetch deepen-not", name)?;
1930            request
1931                .arguments
1932                .push(format!("deepen-not {name}").into_bytes());
1933        }
1934        if self.deepen_relative {
1935            request.arguments.push(b"deepen-relative".to_vec());
1936        }
1937        if let Some(filter) = &self.filter {
1938            validate_protocol_v2_token("fetch filter", filter)?;
1939            request
1940                .arguments
1941                .push(format!("filter {filter}").into_bytes());
1942        }
1943        if let Some(protocols) = &self.packfile_uris {
1944            validate_protocol_v2_token("fetch packfile-uris", protocols)?;
1945            request
1946                .arguments
1947                .push(format!("packfile-uris {protocols}").into_bytes());
1948        }
1949        if self.thin_pack {
1950            request.arguments.push(b"thin-pack".to_vec());
1951        }
1952        if self.no_progress {
1953            request.arguments.push(b"no-progress".to_vec());
1954        }
1955        if self.include_tag {
1956            request.arguments.push(b"include-tag".to_vec());
1957        }
1958        if self.ofs_delta {
1959            request.arguments.push(b"ofs-delta".to_vec());
1960        }
1961        if self.sideband_all {
1962            request.arguments.push(b"sideband-all".to_vec());
1963        }
1964        if self.wait_for_done {
1965            request.arguments.push(b"wait-for-done".to_vec());
1966        }
1967        if self.done {
1968            request.arguments.push(b"done".to_vec());
1969        }
1970        Ok(request)
1971    }
1972}
1973
1974impl ProtocolV2ObjectInfoRequest {
1975    pub fn from_command_request(
1976        format: ObjectFormat,
1977        request: &ProtocolV2CommandRequest,
1978    ) -> Result<Self> {
1979        if request.command != "object-info" {
1980            return Err(GitError::InvalidFormat(format!(
1981                "expected object-info command, got {}",
1982                request.command
1983            )));
1984        }
1985        let mut out = Self::default();
1986        for argument in &request.arguments {
1987            let text = parse_protocol_v2_line_text("object-info request argument", argument)?;
1988            if text == "size" {
1989                if out.size {
1990                    return Err(GitError::InvalidFormat(
1991                        "object-info request has duplicate size argument".into(),
1992                    ));
1993                }
1994                out.size = true;
1995            } else if text.starts_with("oid ") {
1996                out.oids
1997                    .push(parse_oid_argument(format, "object-info oid", text, "oid ")?);
1998            } else {
1999                return Err(GitError::InvalidFormat(format!(
2000                    "unsupported object-info request argument {text}"
2001                )));
2002            }
2003        }
2004        if !out.size {
2005            return Err(GitError::InvalidFormat(
2006                "object-info request is missing size argument".into(),
2007            ));
2008        }
2009        if out.oids.is_empty() {
2010            return Err(GitError::InvalidFormat(
2011                "object-info request is missing object ids".into(),
2012            ));
2013        }
2014        Ok(out)
2015    }
2016
2017    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
2018        if !self.size {
2019            return Err(GitError::InvalidFormat(
2020                "object-info request is missing size argument".into(),
2021            ));
2022        }
2023        if self.oids.is_empty() {
2024            return Err(GitError::InvalidFormat(
2025                "object-info request is missing object ids".into(),
2026            ));
2027        }
2028        let mut request = ProtocolV2CommandRequest::new("object-info")?;
2029        request.arguments.push(b"size".to_vec());
2030        for oid in &self.oids {
2031            request.arguments.push(format!("oid {oid}").into_bytes());
2032        }
2033        Ok(request)
2034    }
2035}
2036
2037pub fn parse_protocol_v2_advertisement(frames: &[PktLineFrame]) -> Result<TransportHandshake> {
2038    let Some((first, rest)) = frames.split_first() else {
2039        return Err(GitError::InvalidFormat(
2040            "protocol v2 advertisement is empty".into(),
2041        ));
2042    };
2043    match first {
2044        PktLineFrame::Data(payload) if trim_trailing_lf(payload) == b"version 2" => {}
2045        PktLineFrame::Data(_) => {
2046            return Err(GitError::InvalidFormat(
2047                "protocol v2 advertisement missing version line".into(),
2048            ));
2049        }
2050        _ => {
2051            return Err(GitError::InvalidFormat(
2052                "protocol v2 advertisement must start with a data line".into(),
2053            ));
2054        }
2055    }
2056
2057    let mut capabilities = Vec::new();
2058    let mut saw_flush = false;
2059    for (idx, frame) in rest.iter().enumerate() {
2060        match frame {
2061            PktLineFrame::Data(payload) => {
2062                if saw_flush {
2063                    return Err(GitError::InvalidFormat(
2064                        "protocol v2 advertisement has data after flush".into(),
2065                    ));
2066                }
2067                capabilities.push(parse_protocol_v2_capability_line(payload)?);
2068            }
2069            PktLineFrame::Flush => {
2070                saw_flush = true;
2071                if idx + 1 != rest.len() {
2072                    return Err(GitError::InvalidFormat(
2073                        "protocol v2 advertisement has frames after flush".into(),
2074                    ));
2075                }
2076            }
2077            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
2078                return Err(GitError::InvalidFormat(
2079                    "protocol v2 advertisement contains a non-flush control packet".into(),
2080                ));
2081            }
2082        }
2083    }
2084    if !saw_flush {
2085        return Err(GitError::InvalidFormat(
2086            "protocol v2 advertisement missing flush".into(),
2087        ));
2088    }
2089
2090    Ok(TransportHandshake {
2091        protocol: ProtocolVersion::V2,
2092        capabilities,
2093    })
2094}
2095
2096pub fn encode_protocol_v2_advertisement(
2097    handshake: &TransportHandshake,
2098) -> Result<Vec<PktLineFrame>> {
2099    if handshake.protocol != ProtocolVersion::V2 {
2100        return Err(GitError::InvalidFormat(
2101            "protocol v2 advertisement requires a v2 handshake".into(),
2102        ));
2103    }
2104    let mut frames = vec![PktLineFrame::data(line_from_str("version 2"))?];
2105    for capability in &handshake.capabilities {
2106        frames.push(PktLineFrame::data(line(encode_protocol_v2_capability(
2107            capability,
2108        )?))?);
2109    }
2110    frames.push(PktLineFrame::Flush);
2111    Ok(frames)
2112}
2113
2114pub fn read_protocol_v2_advertisement(reader: &mut impl Read) -> Result<TransportHandshake> {
2115    let frames = read_pkt_line_frames_until_flush(reader)?;
2116    parse_protocol_v2_advertisement(&frames)
2117}
2118
2119pub fn write_protocol_v2_advertisement(
2120    writer: &mut impl Write,
2121    handshake: &TransportHandshake,
2122) -> Result<()> {
2123    if handshake.protocol != ProtocolVersion::V2 {
2124        return Err(GitError::InvalidFormat(
2125            "protocol v2 advertisement requires a v2 handshake".into(),
2126        ));
2127    }
2128    write_pkt_line_payload(writer, b"version 2\n")?;
2129    for capability in &handshake.capabilities {
2130        write_pkt_line_payload(writer, &line(encode_protocol_v2_capability(capability)?))?;
2131    }
2132    writer.write_all(b"0000")?;
2133    Ok(())
2134}
2135
2136pub fn parse_protocol_v2_command_request(
2137    frames: &[PktLineFrame],
2138) -> Result<ProtocolV2CommandRequest> {
2139    let Some((first, rest)) = frames.split_first() else {
2140        return Err(GitError::InvalidFormat(
2141            "protocol v2 command request is empty".into(),
2142        ));
2143    };
2144    let command = match first {
2145        PktLineFrame::Data(payload) => parse_protocol_v2_command_line(payload)?,
2146        _ => {
2147            return Err(GitError::InvalidFormat(
2148                "protocol v2 command request must start with a command line".into(),
2149            ));
2150        }
2151    };
2152
2153    let mut capabilities = Vec::new();
2154    let mut arguments = Vec::new();
2155    let mut in_arguments = false;
2156    let mut saw_flush = false;
2157    for (idx, frame) in rest.iter().enumerate() {
2158        match frame {
2159            PktLineFrame::Data(payload) if !in_arguments => {
2160                if saw_flush {
2161                    return Err(GitError::InvalidFormat(
2162                        "protocol v2 command request has data after flush".into(),
2163                    ));
2164                }
2165                capabilities.push(parse_protocol_v2_capability_line(payload)?);
2166            }
2167            PktLineFrame::Data(payload) => {
2168                if saw_flush {
2169                    return Err(GitError::InvalidFormat(
2170                        "protocol v2 command request has data after flush".into(),
2171                    ));
2172                }
2173                let argument = trim_trailing_lf(payload);
2174                if argument.is_empty() {
2175                    return Err(GitError::InvalidFormat(
2176                        "protocol v2 command argument is empty".into(),
2177                    ));
2178                }
2179                if argument
2180                    .iter()
2181                    .any(|byte| matches!(*byte, b'\n' | b'\r' | 0))
2182                {
2183                    return Err(GitError::InvalidFormat(
2184                        "protocol v2 command argument contains a delimiter byte".into(),
2185                    ));
2186                }
2187                arguments.push(argument.to_vec());
2188            }
2189            PktLineFrame::Delimiter => {
2190                if in_arguments {
2191                    return Err(GitError::InvalidFormat(
2192                        "protocol v2 command request has duplicate delimiter".into(),
2193                    ));
2194                }
2195                if saw_flush {
2196                    return Err(GitError::InvalidFormat(
2197                        "protocol v2 command request has delimiter after flush".into(),
2198                    ));
2199                }
2200                in_arguments = true;
2201            }
2202            PktLineFrame::Flush => {
2203                saw_flush = true;
2204                if idx + 1 != rest.len() {
2205                    return Err(GitError::InvalidFormat(
2206                        "protocol v2 command request has frames after flush".into(),
2207                    ));
2208                }
2209            }
2210            PktLineFrame::ResponseEnd => {
2211                return Err(GitError::InvalidFormat(
2212                    "protocol v2 command request contains response-end".into(),
2213                ));
2214            }
2215        }
2216    }
2217    if !saw_flush {
2218        return Err(GitError::InvalidFormat(
2219            "protocol v2 command request missing flush".into(),
2220        ));
2221    }
2222
2223    Ok(ProtocolV2CommandRequest {
2224        command,
2225        capabilities,
2226        arguments,
2227    })
2228}
2229
2230pub fn encode_protocol_v2_command_request(
2231    request: &ProtocolV2CommandRequest,
2232) -> Result<Vec<PktLineFrame>> {
2233    validate_capability_name(&request.command)?;
2234    let mut frames = Vec::new();
2235    frames.push(PktLineFrame::data(line_from_str(&format!(
2236        "command={}",
2237        request.command
2238    )))?);
2239    for capability in &request.capabilities {
2240        frames.push(PktLineFrame::data(line(encode_protocol_v2_capability(
2241            capability,
2242        )?))?);
2243    }
2244    if !request.arguments.is_empty() {
2245        frames.push(PktLineFrame::Delimiter);
2246        for argument in &request.arguments {
2247            validate_protocol_v2_argument(argument)?;
2248            let mut payload = argument.clone();
2249            payload.push(b'\n');
2250            frames.push(PktLineFrame::data(payload)?);
2251        }
2252    }
2253    frames.push(PktLineFrame::Flush);
2254    Ok(frames)
2255}
2256
2257pub fn parse_protocol_v2_request(frames: &[PktLineFrame]) -> Result<ProtocolV2Request> {
2258    if matches!(frames, [PktLineFrame::Flush]) {
2259        return Ok(ProtocolV2Request::Done);
2260    }
2261    parse_protocol_v2_command_request(frames).map(ProtocolV2Request::Command)
2262}
2263
2264pub fn encode_protocol_v2_request(request: &ProtocolV2Request) -> Result<Vec<PktLineFrame>> {
2265    match request {
2266        ProtocolV2Request::Command(command) => encode_protocol_v2_command_request(command),
2267        ProtocolV2Request::Done => Ok(vec![PktLineFrame::Flush]),
2268    }
2269}
2270
2271pub fn read_protocol_v2_request(reader: &mut impl Read) -> Result<ProtocolV2Request> {
2272    let frames = read_pkt_line_frames_until_flush(reader)?;
2273    parse_protocol_v2_request(&frames)
2274}
2275
2276pub fn write_protocol_v2_request(
2277    writer: &mut impl Write,
2278    request: &ProtocolV2Request,
2279) -> Result<()> {
2280    match request {
2281        ProtocolV2Request::Command(command) => write_protocol_v2_command_request(writer, command),
2282        ProtocolV2Request::Done => {
2283            writer.write_all(b"0000")?;
2284            Ok(())
2285        }
2286    }
2287}
2288
2289pub fn read_protocol_v2_command_request(
2290    reader: &mut impl Read,
2291) -> Result<ProtocolV2CommandRequest> {
2292    let frames = read_pkt_line_frames_until_flush(reader)?;
2293    parse_protocol_v2_command_request(&frames)
2294}
2295
2296pub fn write_protocol_v2_command_request(
2297    writer: &mut impl Write,
2298    request: &ProtocolV2CommandRequest,
2299) -> Result<()> {
2300    validate_capability_name(&request.command)?;
2301    write_pkt_line_payload(
2302        writer,
2303        &line_from_str(&format!("command={}", request.command)),
2304    )?;
2305    for capability in &request.capabilities {
2306        write_pkt_line_payload(writer, &line(encode_protocol_v2_capability(capability)?))?;
2307    }
2308    if !request.arguments.is_empty() {
2309        write_pkt_line_frame(writer, &PktLineFrame::Delimiter)?;
2310        for argument in &request.arguments {
2311            validate_protocol_v2_argument(argument)?;
2312            let mut payload = argument.clone();
2313            payload.push(b'\n');
2314            write_pkt_line_payload(writer, &payload)?;
2315        }
2316    }
2317    writer.write_all(b"0000")?;
2318    Ok(())
2319}
2320
2321pub fn read_protocol_v2_ls_refs_request(reader: &mut impl Read) -> Result<ProtocolV2LsRefsRequest> {
2322    let request = read_protocol_v2_command_request(reader)?;
2323    ProtocolV2LsRefsRequest::from_command_request(&request)
2324}
2325
2326pub fn write_protocol_v2_ls_refs_request(
2327    writer: &mut impl Write,
2328    request: &ProtocolV2LsRefsRequest,
2329) -> Result<()> {
2330    let command = request.to_command_request()?;
2331    write_protocol_v2_command_request(writer, &command)
2332}
2333
2334pub fn parse_protocol_v2_ls_refs_response(
2335    format: ObjectFormat,
2336    frames: &[PktLineFrame],
2337) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2338    let mut records = Vec::new();
2339    let mut saw_flush = false;
2340    for (idx, frame) in frames.iter().enumerate() {
2341        match frame {
2342            PktLineFrame::Data(payload) => {
2343                if saw_flush {
2344                    return Err(GitError::InvalidFormat(
2345                        "ls-refs response has data after flush".into(),
2346                    ));
2347                }
2348                records.push(parse_protocol_v2_ls_refs_line(format, payload)?);
2349            }
2350            PktLineFrame::Flush => {
2351                saw_flush = true;
2352                if !flush_terminates_protocol_v2_response(frames, idx) {
2353                    return Err(GitError::InvalidFormat(
2354                        "ls-refs response has frames after flush".into(),
2355                    ));
2356                }
2357            }
2358            PktLineFrame::ResponseEnd if saw_flush && idx + 1 == frames.len() => {}
2359            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
2360                return Err(GitError::InvalidFormat(
2361                    "ls-refs response contains a non-flush control packet".into(),
2362                ));
2363            }
2364        }
2365    }
2366    if !saw_flush {
2367        return Err(GitError::InvalidFormat(
2368            "ls-refs response missing flush".into(),
2369        ));
2370    }
2371    Ok(records)
2372}
2373
2374pub fn encode_protocol_v2_ls_refs_response(
2375    records: &[ProtocolV2LsRefsRecord],
2376) -> Result<Vec<PktLineFrame>> {
2377    let mut frames = Vec::new();
2378    for record in records {
2379        frames.push(PktLineFrame::data(line_from_str(
2380            &format_protocol_v2_ls_refs_record(record)?,
2381        ))?);
2382    }
2383    frames.push(PktLineFrame::Flush);
2384    Ok(frames)
2385}
2386
2387pub fn read_protocol_v2_ls_refs_response(
2388    format: ObjectFormat,
2389    reader: &mut impl Read,
2390) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2391    let frames = read_pkt_line_frames_until_flush(reader)?;
2392    parse_protocol_v2_ls_refs_response(format, &frames)
2393}
2394
2395pub fn write_protocol_v2_ls_refs_response(
2396    writer: &mut impl Write,
2397    records: &[ProtocolV2LsRefsRecord],
2398) -> Result<()> {
2399    for record in records {
2400        write_pkt_line_payload(
2401            writer,
2402            &line_from_str(&format_protocol_v2_ls_refs_record(record)?),
2403        )?;
2404    }
2405    writer.write_all(b"0000")?;
2406    Ok(())
2407}
2408
2409pub fn read_protocol_v2_ls_refs_response_until_response_end(
2410    format: ObjectFormat,
2411    reader: &mut impl Read,
2412) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2413    let frames = read_pkt_line_frames_until_response_end(reader)?;
2414    parse_protocol_v2_ls_refs_response(format, &frames)
2415}
2416
2417pub fn write_protocol_v2_ls_refs_response_with_response_end(
2418    writer: &mut impl Write,
2419    records: &[ProtocolV2LsRefsRecord],
2420) -> Result<()> {
2421    write_protocol_v2_ls_refs_response(writer, records)?;
2422    writer.write_all(b"0002")?;
2423    Ok(())
2424}
2425
2426pub fn exchange_protocol_v2_ls_refs(
2427    format: ObjectFormat,
2428    reader: &mut impl Read,
2429    writer: &mut impl Write,
2430    request: &ProtocolV2LsRefsRequest,
2431) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2432    write_protocol_v2_ls_refs_request(writer, request)?;
2433    writer.flush()?;
2434    read_protocol_v2_ls_refs_response(format, reader)
2435}
2436
2437/// Bridge a parsed protocol v2 `ls-refs` response into the shared
2438/// [`RefAdvertisementSet`]/[`RefAdvertisement`] types used by the v0/v1 codecs,
2439/// so callers can drive v2 clone/fetch through the same ref-advertisement
2440/// machinery.
2441///
2442/// Each [`ProtocolV2LsRefsRecord::Ref`] becomes a [`RefAdvertisement`]. A
2443/// `peeled:<oid>` attribute is emitted as an additional `<peeled-oid>
2444/// <name>^{}` advertisement, matching the v0/v1 peeled-tag convention.
2445/// `symref-target:<target>` attributes are collected as `symref=<name>:<target>`
2446/// capabilities on the first advertised ref, mirroring how the upload-pack v0/v1
2447/// advertisement carries symrefs. [`ProtocolV2LsRefsRecord::Unborn`] records have
2448/// no object id, so they cannot be represented as a [`RefAdvertisement`]; an
2449/// unborn record carrying a `symref-target` is preserved as a `symref` capability
2450/// while otherwise being skipped. The returned set always reports
2451/// [`ProtocolVersion::V2`].
2452pub fn protocol_v2_ls_refs_records_to_ref_advertisement_set(
2453    records: &[ProtocolV2LsRefsRecord],
2454) -> Result<RefAdvertisementSet> {
2455    let mut refs: Vec<RefAdvertisement> = Vec::new();
2456    let mut symrefs: Vec<Capability> = Vec::new();
2457    for record in records {
2458        match record {
2459            ProtocolV2LsRefsRecord::Ref(reference) => {
2460                validate_protocol_v2_token("ls-refs ref name", &reference.name)?;
2461                refs.push(RefAdvertisement {
2462                    oid: reference.oid,
2463                    name: reference.name.clone(),
2464                    capabilities: Vec::new(),
2465                });
2466                if let Some(peeled) = &reference.peeled {
2467                    refs.push(RefAdvertisement {
2468                        oid: peeled.clone(),
2469                        name: format!("{}^{{}}", reference.name),
2470                        capabilities: Vec::new(),
2471                    });
2472                }
2473                if let Some(target) = &reference.symref_target {
2474                    symrefs.push(protocol_v2_symref_capability(&reference.name, target)?);
2475                }
2476            }
2477            ProtocolV2LsRefsRecord::Unborn {
2478                name,
2479                symref_target,
2480                ..
2481            } => {
2482                validate_protocol_v2_token("ls-refs ref name", name)?;
2483                if let Some(target) = symref_target {
2484                    symrefs.push(protocol_v2_symref_capability(name, target)?);
2485                }
2486            }
2487        }
2488    }
2489    if !symrefs.is_empty() {
2490        if let Some(first) = refs.first_mut() {
2491            first.capabilities = symrefs;
2492        } else {
2493            return Err(GitError::InvalidFormat(
2494                "ls-refs response advertised symrefs without any concrete refs".into(),
2495            ));
2496        }
2497    }
2498    Ok(RefAdvertisementSet {
2499        protocol: ProtocolVersion::V2,
2500        refs,
2501        shallow: Vec::new(),
2502    })
2503}
2504
2505/// Parse a protocol v2 `ls-refs` response and bridge it into the shared
2506/// [`RefAdvertisementSet`] type. Convenience wrapper combining
2507/// [`parse_protocol_v2_ls_refs_response`] and
2508/// [`protocol_v2_ls_refs_records_to_ref_advertisement_set`].
2509pub fn parse_protocol_v2_ls_refs_response_as_ref_advertisement_set(
2510    format: ObjectFormat,
2511    frames: &[PktLineFrame],
2512) -> Result<RefAdvertisementSet> {
2513    let records = parse_protocol_v2_ls_refs_response(format, frames)?;
2514    protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
2515}
2516
2517/// Read a protocol v2 `ls-refs` response from `reader` and bridge it into the
2518/// shared [`RefAdvertisementSet`] type.
2519pub fn read_protocol_v2_ls_refs_response_as_ref_advertisement_set(
2520    format: ObjectFormat,
2521    reader: &mut impl Read,
2522) -> Result<RefAdvertisementSet> {
2523    let records = read_protocol_v2_ls_refs_response(format, reader)?;
2524    protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
2525}
2526
2527fn protocol_v2_symref_capability(name: &str, target: &str) -> Result<Capability> {
2528    validate_protocol_v2_token("ls-refs symref-target", target)?;
2529    Ok(Capability {
2530        name: "symref".into(),
2531        value: Some(format!("{name}:{target}")),
2532    })
2533}
2534
2535pub fn read_protocol_v2_fetch_request(
2536    format: ObjectFormat,
2537    reader: &mut impl Read,
2538) -> Result<ProtocolV2FetchRequest> {
2539    let request = read_protocol_v2_command_request(reader)?;
2540    ProtocolV2FetchRequest::from_command_request(format, &request)
2541}
2542
2543pub fn write_protocol_v2_fetch_request(
2544    writer: &mut impl Write,
2545    request: &ProtocolV2FetchRequest,
2546) -> Result<()> {
2547    let command = request.to_command_request()?;
2548    write_protocol_v2_command_request(writer, &command)
2549}
2550
2551pub fn read_protocol_v2_object_info_request(
2552    format: ObjectFormat,
2553    reader: &mut impl Read,
2554) -> Result<ProtocolV2ObjectInfoRequest> {
2555    let request = read_protocol_v2_command_request(reader)?;
2556    ProtocolV2ObjectInfoRequest::from_command_request(format, &request)
2557}
2558
2559pub fn write_protocol_v2_object_info_request(
2560    writer: &mut impl Write,
2561    request: &ProtocolV2ObjectInfoRequest,
2562) -> Result<()> {
2563    let command = request.to_command_request()?;
2564    write_protocol_v2_command_request(writer, &command)
2565}
2566
2567pub fn parse_protocol_v2_fetch_response(
2568    format: ObjectFormat,
2569    frames: &[PktLineFrame],
2570) -> Result<Vec<ProtocolV2FetchResponseSection>> {
2571    let mut sections = Vec::new();
2572    let mut current: Option<(String, Vec<Vec<u8>>)> = None;
2573    let mut saw_flush = false;
2574    for (idx, frame) in frames.iter().enumerate() {
2575        match frame {
2576            PktLineFrame::Data(payload) => {
2577                if saw_flush {
2578                    return Err(GitError::InvalidFormat(
2579                        "fetch response has data after flush".into(),
2580                    ));
2581                }
2582                if let Some((_name, lines)) = &mut current {
2583                    lines.push(payload.clone());
2584                } else {
2585                    let name = parse_fetch_section_header(payload)?;
2586                    current = Some((name, Vec::new()));
2587                }
2588            }
2589            PktLineFrame::Delimiter => {
2590                if saw_flush {
2591                    return Err(GitError::InvalidFormat(
2592                        "fetch response has delimiter after flush".into(),
2593                    ));
2594                }
2595                let Some((name, lines)) = current.take() else {
2596                    return Err(GitError::InvalidFormat(
2597                        "fetch response has delimiter before section".into(),
2598                    ));
2599                };
2600                sections.push(parse_fetch_section(format, name, lines)?);
2601            }
2602            PktLineFrame::Flush => {
2603                saw_flush = true;
2604                if !flush_terminates_protocol_v2_response(frames, idx) {
2605                    return Err(GitError::InvalidFormat(
2606                        "fetch response has frames after flush".into(),
2607                    ));
2608                }
2609                if let Some((name, lines)) = current.take() {
2610                    sections.push(parse_fetch_section(format, name, lines)?);
2611                }
2612            }
2613            PktLineFrame::ResponseEnd if saw_flush && idx + 1 == frames.len() => {}
2614            PktLineFrame::ResponseEnd => {
2615                return Err(GitError::InvalidFormat(
2616                    "fetch response contains response-end".into(),
2617                ));
2618            }
2619        }
2620    }
2621    if !saw_flush {
2622        return Err(GitError::InvalidFormat(
2623            "fetch response missing flush".into(),
2624        ));
2625    }
2626    Ok(sections)
2627}
2628
2629pub fn encode_protocol_v2_fetch_response(
2630    sections: &[ProtocolV2FetchResponseSection],
2631) -> Result<Vec<PktLineFrame>> {
2632    let mut frames = Vec::new();
2633    for (idx, section) in sections.iter().enumerate() {
2634        if idx != 0 {
2635            frames.push(PktLineFrame::Delimiter);
2636        }
2637        frames.push(PktLineFrame::data(line_from_str(
2638            protocol_v2_fetch_section_name(section),
2639        ))?);
2640        for line in format_protocol_v2_fetch_section_lines(section)? {
2641            frames.push(PktLineFrame::data(line)?);
2642        }
2643    }
2644    frames.push(PktLineFrame::Flush);
2645    Ok(frames)
2646}
2647
2648pub fn parse_protocol_v2_fetch_sideband_all_response(
2649    format: ObjectFormat,
2650    frames: &[PktLineFrame],
2651) -> Result<ProtocolV2FetchSidebandAllResponse> {
2652    let mut demuxed = Vec::new();
2653    let mut progress = Vec::new();
2654    let mut in_packfile = false;
2655    for frame in frames {
2656        match frame {
2657            PktLineFrame::Data(payload) if in_packfile => {
2658                demuxed.push(PktLineFrame::Data(payload.clone()));
2659            }
2660            PktLineFrame::Data(payload) => {
2661                let packet = parse_sideband_packet(payload)?;
2662                match packet.channel {
2663                    SideBandChannel::Data => {
2664                        if trim_trailing_lf(&packet.data) == b"packfile" {
2665                            in_packfile = true;
2666                        }
2667                        demuxed.push(PktLineFrame::Data(packet.data));
2668                    }
2669                    SideBandChannel::Progress => progress.push(packet.data),
2670                    SideBandChannel::Fatal => {
2671                        let message = String::from_utf8_lossy(&packet.data).into_owned();
2672                        return Err(GitError::InvalidFormat(format!(
2673                            "sideband fatal: {message}"
2674                        )));
2675                    }
2676                }
2677            }
2678            PktLineFrame::Delimiter => {
2679                in_packfile = false;
2680                demuxed.push(PktLineFrame::Delimiter);
2681            }
2682            PktLineFrame::Flush => {
2683                in_packfile = false;
2684                demuxed.push(PktLineFrame::Flush);
2685            }
2686            PktLineFrame::ResponseEnd => {
2687                in_packfile = false;
2688                demuxed.push(PktLineFrame::ResponseEnd);
2689            }
2690        }
2691    }
2692    Ok(ProtocolV2FetchSidebandAllResponse {
2693        sections: parse_protocol_v2_fetch_response(format, &demuxed)?,
2694        progress,
2695    })
2696}
2697
2698pub fn encode_protocol_v2_fetch_sideband_all_response(
2699    sections: &[ProtocolV2FetchResponseSection],
2700) -> Result<Vec<PktLineFrame>> {
2701    let frames = encode_protocol_v2_fetch_response(sections)?;
2702    let mut encoded = Vec::new();
2703    let mut in_packfile = false;
2704    for frame in frames {
2705        match frame {
2706            PktLineFrame::Data(payload) if in_packfile => {
2707                encoded.push(PktLineFrame::Data(payload));
2708            }
2709            PktLineFrame::Data(payload) => {
2710                if trim_trailing_lf(&payload) == b"packfile" {
2711                    in_packfile = true;
2712                }
2713                encoded.push(PktLineFrame::data(encode_sideband_packet(
2714                    &SideBandPacket {
2715                        channel: SideBandChannel::Data,
2716                        data: payload,
2717                    },
2718                )?)?);
2719            }
2720            PktLineFrame::Delimiter => {
2721                in_packfile = false;
2722                encoded.push(PktLineFrame::Delimiter);
2723            }
2724            PktLineFrame::Flush => {
2725                in_packfile = false;
2726                encoded.push(PktLineFrame::Flush);
2727            }
2728            PktLineFrame::ResponseEnd => {
2729                in_packfile = false;
2730                encoded.push(PktLineFrame::ResponseEnd);
2731            }
2732        }
2733    }
2734    Ok(encoded)
2735}
2736
2737pub fn read_protocol_v2_fetch_response(
2738    format: ObjectFormat,
2739    reader: &mut impl Read,
2740) -> Result<Vec<ProtocolV2FetchResponseSection>> {
2741    let frames = read_pkt_line_frames_until_flush(reader)?;
2742    parse_protocol_v2_fetch_response(format, &frames)
2743}
2744
2745pub fn write_protocol_v2_fetch_response(
2746    writer: &mut impl Write,
2747    sections: &[ProtocolV2FetchResponseSection],
2748) -> Result<()> {
2749    write_protocol_v2_fetch_response_inner(writer, sections, false, false)
2750}
2751
2752pub fn read_protocol_v2_fetch_sideband_all_response(
2753    format: ObjectFormat,
2754    reader: &mut impl Read,
2755) -> Result<ProtocolV2FetchSidebandAllResponse> {
2756    let frames = read_pkt_line_frames_until_flush(reader)?;
2757    parse_protocol_v2_fetch_sideband_all_response(format, &frames)
2758}
2759
2760pub fn write_protocol_v2_fetch_sideband_all_response(
2761    writer: &mut impl Write,
2762    sections: &[ProtocolV2FetchResponseSection],
2763) -> Result<()> {
2764    write_protocol_v2_fetch_response_inner(writer, sections, true, false)
2765}
2766
2767pub fn read_protocol_v2_fetch_response_until_response_end(
2768    format: ObjectFormat,
2769    reader: &mut impl Read,
2770) -> Result<Vec<ProtocolV2FetchResponseSection>> {
2771    let frames = read_pkt_line_frames_until_response_end(reader)?;
2772    parse_protocol_v2_fetch_response(format, &frames)
2773}
2774
2775pub fn write_protocol_v2_fetch_response_with_response_end(
2776    writer: &mut impl Write,
2777    sections: &[ProtocolV2FetchResponseSection],
2778) -> Result<()> {
2779    write_protocol_v2_fetch_response_inner(writer, sections, false, true)
2780}
2781
2782pub fn read_protocol_v2_fetch_sideband_all_response_until_response_end(
2783    format: ObjectFormat,
2784    reader: &mut impl Read,
2785) -> Result<ProtocolV2FetchSidebandAllResponse> {
2786    let frames = read_pkt_line_frames_until_response_end(reader)?;
2787    parse_protocol_v2_fetch_sideband_all_response(format, &frames)
2788}
2789
2790pub fn write_protocol_v2_fetch_sideband_all_response_with_response_end(
2791    writer: &mut impl Write,
2792    sections: &[ProtocolV2FetchResponseSection],
2793) -> Result<()> {
2794    write_protocol_v2_fetch_response_inner(writer, sections, true, true)
2795}
2796
2797fn write_protocol_v2_fetch_response_inner(
2798    writer: &mut impl Write,
2799    sections: &[ProtocolV2FetchResponseSection],
2800    sideband_all: bool,
2801    response_end: bool,
2802) -> Result<()> {
2803    let mut in_packfile = false;
2804    for (idx, section) in sections.iter().enumerate() {
2805        if idx != 0 {
2806            in_packfile = false;
2807            write_pkt_line_frame(writer, &PktLineFrame::Delimiter)?;
2808        }
2809        write_protocol_v2_fetch_payload(
2810            writer,
2811            &line_from_str(protocol_v2_fetch_section_name(section)),
2812            sideband_all,
2813            &mut in_packfile,
2814        )?;
2815        for payload in format_protocol_v2_fetch_section_lines(section)? {
2816            write_protocol_v2_fetch_payload(writer, &payload, sideband_all, &mut in_packfile)?;
2817        }
2818    }
2819    writer.write_all(b"0000")?;
2820    if response_end {
2821        writer.write_all(b"0002")?;
2822    }
2823    Ok(())
2824}
2825
2826fn write_protocol_v2_fetch_payload(
2827    writer: &mut impl Write,
2828    payload: &[u8],
2829    sideband_all: bool,
2830    in_packfile: &mut bool,
2831) -> Result<()> {
2832    if sideband_all && !*in_packfile {
2833        if trim_trailing_lf(payload) == b"packfile" {
2834            *in_packfile = true;
2835        }
2836        write_sideband_payload(writer, SideBandChannel::Data, payload)
2837    } else {
2838        write_pkt_line_payload(writer, payload)
2839    }
2840}
2841
2842pub fn exchange_protocol_v2_fetch(
2843    format: ObjectFormat,
2844    reader: &mut impl Read,
2845    writer: &mut impl Write,
2846    request: &ProtocolV2FetchRequest,
2847) -> Result<Vec<ProtocolV2FetchResponseSection>> {
2848    write_protocol_v2_fetch_request(writer, request)?;
2849    writer.flush()?;
2850    read_protocol_v2_fetch_response(format, reader)
2851}
2852
2853pub fn parse_protocol_v2_object_info_response(
2854    format: ObjectFormat,
2855    frames: &[PktLineFrame],
2856) -> Result<ProtocolV2ObjectInfoResponse> {
2857    let Some((first, rest)) = frames.split_first() else {
2858        return Err(GitError::InvalidFormat(
2859            "object-info response is empty".into(),
2860        ));
2861    };
2862    let PktLineFrame::Data(attrs) = first else {
2863        return Err(GitError::InvalidFormat(
2864            "object-info response must start with attributes".into(),
2865        ));
2866    };
2867    let attrs = parse_protocol_v2_line_text("object-info response attributes", attrs)?;
2868    let mut response = ProtocolV2ObjectInfoResponse::default();
2869    for attr in attrs.split(' ') {
2870        validate_protocol_v2_token("object-info response attribute", attr)?;
2871        match attr {
2872            "size" => {
2873                if response.size {
2874                    return Err(GitError::InvalidFormat(
2875                        "object-info response has duplicate size attribute".into(),
2876                    ));
2877                }
2878                response.size = true;
2879            }
2880            other => {
2881                return Err(GitError::InvalidFormat(format!(
2882                    "unsupported object-info response attribute {other}"
2883                )));
2884            }
2885        }
2886    }
2887    if !response.size {
2888        return Err(GitError::InvalidFormat(
2889            "object-info response is missing size attribute".into(),
2890        ));
2891    }
2892
2893    let mut saw_flush = false;
2894    for (idx, frame) in rest.iter().enumerate() {
2895        match frame {
2896            PktLineFrame::Data(payload) if !saw_flush => {
2897                response
2898                    .records
2899                    .push(parse_protocol_v2_object_info_record(format, payload)?);
2900            }
2901            PktLineFrame::Data(_) => {
2902                return Err(GitError::InvalidFormat(
2903                    "object-info response has data after flush".into(),
2904                ));
2905            }
2906            PktLineFrame::Flush => {
2907                saw_flush = true;
2908                if idx + 1 != rest.len() {
2909                    return Err(GitError::InvalidFormat(
2910                        "object-info response has frames after flush".into(),
2911                    ));
2912                }
2913            }
2914            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
2915                return Err(GitError::InvalidFormat(
2916                    "object-info response contains a non-flush control packet".into(),
2917                ));
2918            }
2919        }
2920    }
2921    if !saw_flush {
2922        return Err(GitError::InvalidFormat(
2923            "object-info response missing flush".into(),
2924        ));
2925    }
2926    Ok(response)
2927}
2928
2929pub fn encode_protocol_v2_object_info_response(
2930    response: &ProtocolV2ObjectInfoResponse,
2931) -> Result<Vec<PktLineFrame>> {
2932    if !response.size {
2933        return Err(GitError::InvalidFormat(
2934            "object-info response is missing size attribute".into(),
2935        ));
2936    }
2937    let mut frames = Vec::new();
2938    frames.push(PktLineFrame::data(line_from_str("size"))?);
2939    for record in &response.records {
2940        frames.push(PktLineFrame::data(line_from_str(&format!(
2941            "{} {}",
2942            record.oid, record.size
2943        )))?);
2944    }
2945    frames.push(PktLineFrame::Flush);
2946    Ok(frames)
2947}
2948
2949pub fn read_protocol_v2_object_info_response(
2950    format: ObjectFormat,
2951    reader: &mut impl Read,
2952) -> Result<ProtocolV2ObjectInfoResponse> {
2953    let frames = read_pkt_line_frames_until_flush(reader)?;
2954    parse_protocol_v2_object_info_response(format, &frames)
2955}
2956
2957pub fn write_protocol_v2_object_info_response(
2958    writer: &mut impl Write,
2959    response: &ProtocolV2ObjectInfoResponse,
2960) -> Result<()> {
2961    if !response.size {
2962        return Err(GitError::InvalidFormat(
2963            "object-info response is missing size attribute".into(),
2964        ));
2965    }
2966    write_pkt_line_payload(writer, b"size\n")?;
2967    for record in &response.records {
2968        write_pkt_line_payload(
2969            writer,
2970            &line_from_str(&format!("{} {}", record.oid, record.size)),
2971        )?;
2972    }
2973    writer.write_all(b"0000")?;
2974    Ok(())
2975}
2976
2977pub fn exchange_protocol_v2_object_info(
2978    format: ObjectFormat,
2979    reader: &mut impl Read,
2980    writer: &mut impl Write,
2981    request: &ProtocolV2ObjectInfoRequest,
2982) -> Result<ProtocolV2ObjectInfoResponse> {
2983    write_protocol_v2_object_info_request(writer, request)?;
2984    writer.flush()?;
2985    read_protocol_v2_object_info_response(format, reader)
2986}
2987
2988pub fn demux_protocol_v2_fetch_packfile(
2989    sections: &[ProtocolV2FetchResponseSection],
2990) -> Result<Option<SideBandDemux>> {
2991    let mut packfile = None;
2992    for section in sections {
2993        if let ProtocolV2FetchResponseSection::Packfile(lines) = section {
2994            if packfile.is_some() {
2995                return Err(GitError::InvalidFormat(
2996                    "fetch response has duplicate packfile sections".into(),
2997                ));
2998            }
2999            packfile = Some(parse_and_demux_sideband_packets(lines)?);
3000        }
3001    }
3002    Ok(packfile)
3003}
3004
3005pub fn protocol_v2_object_format(capabilities: &[Capability]) -> Result<ObjectFormat> {
3006    let mut format = None;
3007    for capability in capabilities {
3008        if capability.name != "object-format" {
3009            continue;
3010        }
3011        if format.is_some() {
3012            return Err(GitError::InvalidFormat(
3013                "protocol v2 has duplicate object-format capabilities".into(),
3014            ));
3015        }
3016        let Some(value) = &capability.value else {
3017            return Err(GitError::InvalidFormat(
3018                "protocol v2 object-format capability is missing a value".into(),
3019            ));
3020        };
3021        format = Some(value.parse::<ObjectFormat>()?);
3022    }
3023    Ok(format.unwrap_or(ObjectFormat::Sha1))
3024}
3025
3026pub fn validate_protocol_v2_command_request_capabilities(
3027    handshake: &TransportHandshake,
3028    request: &ProtocolV2CommandRequest,
3029) -> Result<()> {
3030    if handshake.protocol != ProtocolVersion::V2 {
3031        return Err(GitError::InvalidFormat(
3032            "protocol v2 command validation requires a v2 handshake".into(),
3033        ));
3034    }
3035    let advertised =
3036        protocol_v2_capability(&handshake.capabilities, &request.command).ok_or_else(|| {
3037            GitError::InvalidFormat(format!("unadvertised command {}", request.command))
3038        })?;
3039    if advertised.name.is_empty() {
3040        return Err(GitError::InvalidFormat(
3041            "advertised command capability is empty".into(),
3042        ));
3043    }
3044    parse_protocol_v2_command_options(&request.capabilities)?;
3045
3046    for capability in &request.capabilities {
3047        let advertised = protocol_v2_capability(&handshake.capabilities, &capability.name)
3048            .ok_or_else(|| {
3049                GitError::InvalidFormat(format!(
3050                    "unadvertised protocol v2 capability {}",
3051                    capability.name
3052                ))
3053            })?;
3054        if capability.name == "object-format" {
3055            validate_protocol_v2_object_format_request(advertised, capability)?;
3056        }
3057    }
3058    Ok(())
3059}
3060
3061pub fn parse_protocol_v2_command_options(
3062    capabilities: &[Capability],
3063) -> Result<ProtocolV2CommandOptions> {
3064    let mut out = ProtocolV2CommandOptions::default();
3065    for capability in capabilities {
3066        match capability.name.as_str() {
3067            "agent" => {
3068                if out.agent.is_some() {
3069                    return Err(GitError::InvalidFormat(
3070                        "protocol v2 command has duplicate agent capabilities".into(),
3071                    ));
3072                }
3073                let Some(value) = &capability.value else {
3074                    return Err(GitError::InvalidFormat(
3075                        "protocol v2 agent capability is missing a value".into(),
3076                    ));
3077                };
3078                validate_protocol_v2_capability_value(value)?;
3079                out.agent = Some(value.clone());
3080            }
3081            "object-format" => {
3082                if out.object_format.is_some() {
3083                    return Err(GitError::InvalidFormat(
3084                        "protocol v2 command has duplicate object-format capabilities".into(),
3085                    ));
3086                }
3087                let Some(value) = &capability.value else {
3088                    return Err(GitError::InvalidFormat(
3089                        "protocol v2 object-format capability is missing a value".into(),
3090                    ));
3091                };
3092                out.object_format = Some(value.parse::<ObjectFormat>()?);
3093            }
3094            "server-option" => {
3095                let Some(value) = &capability.value else {
3096                    return Err(GitError::InvalidFormat(
3097                        "protocol v2 server-option capability is missing a value".into(),
3098                    ));
3099                };
3100                validate_protocol_v2_capability_value(value)?;
3101                out.server_options.push(value.clone());
3102            }
3103            _ => out.extra.push(capability.clone()),
3104        }
3105    }
3106    Ok(out)
3107}
3108
3109pub fn encode_protocol_v2_command_options(
3110    options: &ProtocolV2CommandOptions,
3111) -> Result<Vec<Capability>> {
3112    let mut capabilities = Vec::new();
3113    if let Some(agent) = &options.agent {
3114        validate_protocol_v2_capability_value(agent)?;
3115        capabilities.push(Capability {
3116            name: "agent".into(),
3117            value: Some(agent.clone()),
3118        });
3119    }
3120    if let Some(format) = options.object_format {
3121        capabilities.push(Capability {
3122            name: "object-format".into(),
3123            value: Some(format.name().into()),
3124        });
3125    }
3126    for option in &options.server_options {
3127        validate_protocol_v2_capability_value(option)?;
3128        capabilities.push(Capability {
3129            name: "server-option".into(),
3130            value: Some(option.clone()),
3131        });
3132    }
3133    for capability in &options.extra {
3134        if matches!(
3135            capability.name.as_str(),
3136            "agent" | "object-format" | "server-option"
3137        ) {
3138            return Err(GitError::InvalidFormat(format!(
3139                "protocol v2 extra capability duplicates known capability {}",
3140                capability.name
3141            )));
3142        }
3143        encode_protocol_v2_capability(capability)?;
3144        capabilities.push(capability.clone());
3145    }
3146    Ok(capabilities)
3147}
3148
3149pub fn parse_protocol_v2_ls_refs_features(
3150    capabilities: &[Capability],
3151) -> Result<Option<ProtocolV2LsRefsFeatures>> {
3152    let mut ls_refs = None;
3153    for capability in capabilities {
3154        if capability.name != "ls-refs" {
3155            continue;
3156        }
3157        if ls_refs.is_some() {
3158            return Err(GitError::InvalidFormat(
3159                "protocol v2 has duplicate ls-refs capabilities".into(),
3160            ));
3161        }
3162        ls_refs = Some(parse_protocol_v2_ls_refs_feature_value(
3163            capability.value.as_deref(),
3164        )?);
3165    }
3166    Ok(ls_refs)
3167}
3168
3169pub fn encode_protocol_v2_ls_refs_capability(
3170    features: &ProtocolV2LsRefsFeatures,
3171) -> Result<Capability> {
3172    let mut values = Vec::new();
3173    if features.unborn {
3174        values.push("unborn".to_string());
3175    }
3176    for feature in &features.unknown {
3177        validate_protocol_v2_token("ls-refs feature", feature)?;
3178        if feature == "unborn" {
3179            return Err(GitError::InvalidFormat(
3180                "ls-refs unknown features must not duplicate known feature unborn".into(),
3181            ));
3182        }
3183        values.push(feature.clone());
3184    }
3185    Ok(Capability {
3186        name: "ls-refs".into(),
3187        value: (!values.is_empty()).then(|| values.join(" ")),
3188    })
3189}
3190
3191pub fn validate_protocol_v2_ls_refs_request_features(
3192    features: &ProtocolV2LsRefsFeatures,
3193    request: &ProtocolV2LsRefsRequest,
3194) -> Result<()> {
3195    if request.unborn && !features.unborn {
3196        return Err(GitError::InvalidFormat(
3197            "ls-refs request uses unborn without advertised unborn feature".into(),
3198        ));
3199    }
3200    Ok(())
3201}
3202
3203pub fn validate_protocol_v2_ls_refs_command_request(
3204    handshake: &TransportHandshake,
3205    request: &ProtocolV2CommandRequest,
3206) -> Result<ProtocolV2LsRefsRequest> {
3207    validate_protocol_v2_command_request_capabilities(handshake, request)?;
3208    let ls_refs = ProtocolV2LsRefsRequest::from_command_request(request)?;
3209    let features = parse_protocol_v2_ls_refs_features(&handshake.capabilities)?
3210        .ok_or_else(|| GitError::InvalidFormat("ls-refs command was not advertised".into()))?;
3211    validate_protocol_v2_ls_refs_request_features(&features, &ls_refs)?;
3212    Ok(ls_refs)
3213}
3214
3215pub fn parse_protocol_v2_fetch_features(
3216    capabilities: &[Capability],
3217) -> Result<Option<ProtocolV2FetchFeatures>> {
3218    let mut fetch = None;
3219    for capability in capabilities {
3220        if capability.name != "fetch" {
3221            continue;
3222        }
3223        if fetch.is_some() {
3224            return Err(GitError::InvalidFormat(
3225                "protocol v2 has duplicate fetch capabilities".into(),
3226            ));
3227        }
3228        fetch = Some(parse_protocol_v2_fetch_feature_value(
3229            capability.value.as_deref(),
3230        )?);
3231    }
3232    Ok(fetch)
3233}
3234
3235pub fn encode_protocol_v2_fetch_capability(
3236    features: &ProtocolV2FetchFeatures,
3237) -> Result<Capability> {
3238    let mut values = Vec::new();
3239    if features.shallow {
3240        values.push("shallow".to_string());
3241    }
3242    if features.wait_for_done {
3243        values.push("wait-for-done".to_string());
3244    }
3245    if features.filter {
3246        values.push("filter".to_string());
3247    }
3248    if features.ref_in_want {
3249        values.push("ref-in-want".to_string());
3250    }
3251    if features.sideband_all {
3252        values.push("sideband-all".to_string());
3253    }
3254    if features.packfile_uris {
3255        values.push("packfile-uris".to_string());
3256    }
3257    for feature in &features.unknown {
3258        validate_protocol_v2_token("fetch feature", feature)?;
3259        if matches!(
3260            feature.as_str(),
3261            "shallow"
3262                | "wait-for-done"
3263                | "filter"
3264                | "ref-in-want"
3265                | "sideband-all"
3266                | "packfile-uris"
3267        ) {
3268            return Err(GitError::InvalidFormat(format!(
3269                "fetch unknown features must not duplicate known feature {feature}"
3270            )));
3271        }
3272        values.push(feature.clone());
3273    }
3274    Ok(Capability {
3275        name: "fetch".into(),
3276        value: (!values.is_empty()).then(|| values.join(" ")),
3277    })
3278}
3279
3280pub fn validate_protocol_v2_fetch_request_features(
3281    features: &ProtocolV2FetchFeatures,
3282    request: &ProtocolV2FetchRequest,
3283) -> Result<()> {
3284    if !features.shallow
3285        && (!request.shallow.is_empty()
3286            || request.deepen.is_some()
3287            || request.deepen_since.is_some()
3288            || !request.deepen_not.is_empty()
3289            || request.deepen_relative)
3290    {
3291        return Err(GitError::InvalidFormat(
3292            "fetch request uses shallow/deepen arguments without advertised shallow feature".into(),
3293        ));
3294    }
3295    if !features.filter && request.filter.is_some() {
3296        return Err(GitError::InvalidFormat(
3297            "fetch request uses filter without advertised filter feature".into(),
3298        ));
3299    }
3300    if !features.ref_in_want && !request.want_refs.is_empty() {
3301        return Err(GitError::InvalidFormat(
3302            "fetch request uses want-ref without advertised ref-in-want feature".into(),
3303        ));
3304    }
3305    if !features.sideband_all && request.sideband_all {
3306        return Err(GitError::InvalidFormat(
3307            "fetch request uses sideband-all without advertised sideband-all feature".into(),
3308        ));
3309    }
3310    if !features.packfile_uris && request.packfile_uris.is_some() {
3311        return Err(GitError::InvalidFormat(
3312            "fetch request uses packfile-uris without advertised packfile-uris feature".into(),
3313        ));
3314    }
3315    if !features.wait_for_done && request.wait_for_done {
3316        return Err(GitError::InvalidFormat(
3317            "fetch request uses wait-for-done without advertised wait-for-done feature".into(),
3318        ));
3319    }
3320    Ok(())
3321}
3322
3323pub fn validate_protocol_v2_fetch_command_request(
3324    handshake: &TransportHandshake,
3325    format: ObjectFormat,
3326    request: &ProtocolV2CommandRequest,
3327) -> Result<ProtocolV2FetchRequest> {
3328    validate_protocol_v2_command_request_capabilities(handshake, request)?;
3329    let fetch = ProtocolV2FetchRequest::from_command_request(format, request)?;
3330    let features = parse_protocol_v2_fetch_features(&handshake.capabilities)?
3331        .ok_or_else(|| GitError::InvalidFormat("fetch command was not advertised".into()))?;
3332    validate_protocol_v2_fetch_request_features(&features, &fetch)?;
3333    Ok(fetch)
3334}
3335
3336pub fn validate_protocol_v2_object_info_command_request(
3337    handshake: &TransportHandshake,
3338    format: ObjectFormat,
3339    request: &ProtocolV2CommandRequest,
3340) -> Result<ProtocolV2ObjectInfoRequest> {
3341    validate_protocol_v2_command_request_capabilities(handshake, request)?;
3342    let object_info = ProtocolV2ObjectInfoRequest::from_command_request(format, request)?;
3343    protocol_v2_capability(&handshake.capabilities, "object-info")
3344        .ok_or_else(|| GitError::InvalidFormat("object-info command was not advertised".into()))?;
3345    Ok(object_info)
3346}
3347
3348pub fn classify_protocol_v2_command_request(
3349    handshake: &TransportHandshake,
3350    format: ObjectFormat,
3351    request: &ProtocolV2CommandRequest,
3352) -> Result<ProtocolV2Command> {
3353    match request.command.as_str() {
3354        "ls-refs" => validate_protocol_v2_ls_refs_command_request(handshake, request)
3355            .map(ProtocolV2Command::LsRefs),
3356        "fetch" => validate_protocol_v2_fetch_command_request(handshake, format, request)
3357            .map(ProtocolV2Command::Fetch),
3358        "object-info" => {
3359            validate_protocol_v2_object_info_command_request(handshake, format, request)
3360                .map(ProtocolV2Command::ObjectInfo)
3361        }
3362        _ => {
3363            validate_protocol_v2_command_request_capabilities(handshake, request)?;
3364            Ok(ProtocolV2Command::Unknown(request.clone()))
3365        }
3366    }
3367}
3368
3369pub fn classify_protocol_v2_request(
3370    handshake: &TransportHandshake,
3371    format: ObjectFormat,
3372    request: &ProtocolV2Request,
3373) -> Result<ProtocolV2SessionRequest> {
3374    match request {
3375        ProtocolV2Request::Command(command) => {
3376            classify_protocol_v2_command_request(handshake, format, command)
3377                .map(ProtocolV2SessionRequest::Command)
3378        }
3379        ProtocolV2Request::Done => Ok(ProtocolV2SessionRequest::Done),
3380    }
3381}
3382
3383pub fn read_protocol_v2_session_request(
3384    handshake: &TransportHandshake,
3385    format: ObjectFormat,
3386    reader: &mut impl Read,
3387) -> Result<ProtocolV2SessionRequest> {
3388    let request = read_protocol_v2_request(reader)?;
3389    classify_protocol_v2_request(handshake, format, &request)
3390}
3391
3392fn protocol_v2_capability<'a>(
3393    capabilities: &'a [Capability],
3394    name: &str,
3395) -> Option<&'a Capability> {
3396    capabilities
3397        .iter()
3398        .find(|capability| capability.name == name)
3399}
3400
3401fn validate_protocol_v2_object_format_request(
3402    advertised: &Capability,
3403    requested: &Capability,
3404) -> Result<()> {
3405    let Some(advertised) = &advertised.value else {
3406        return Err(GitError::InvalidFormat(
3407            "advertised object-format capability is missing a value".into(),
3408        ));
3409    };
3410    let Some(requested) = &requested.value else {
3411        return Err(GitError::InvalidFormat(
3412            "requested object-format capability is missing a value".into(),
3413        ));
3414    };
3415    if advertised != requested {
3416        return Err(GitError::InvalidFormat(format!(
3417            "requested object-format {requested} does not match advertised {advertised}"
3418        )));
3419    }
3420    Ok(())
3421}
3422
3423fn parse_protocol_v2_ls_refs_feature_value(
3424    value: Option<&str>,
3425) -> Result<ProtocolV2LsRefsFeatures> {
3426    let mut out = ProtocolV2LsRefsFeatures::default();
3427    let Some(value) = value else {
3428        return Ok(out);
3429    };
3430    if value.is_empty() {
3431        return Err(GitError::InvalidFormat(
3432            "protocol v2 ls-refs capability value is empty".into(),
3433        ));
3434    }
3435    for feature in value.split(' ') {
3436        validate_protocol_v2_token("ls-refs feature", feature)?;
3437        match feature {
3438            "unborn" => out.unborn = true,
3439            other => out.unknown.push(other.to_string()),
3440        }
3441    }
3442    Ok(out)
3443}
3444
3445fn parse_protocol_v2_fetch_feature_value(value: Option<&str>) -> Result<ProtocolV2FetchFeatures> {
3446    let mut out = ProtocolV2FetchFeatures::default();
3447    let Some(value) = value else {
3448        return Ok(out);
3449    };
3450    if value.is_empty() {
3451        return Err(GitError::InvalidFormat(
3452            "protocol v2 fetch capability value is empty".into(),
3453        ));
3454    }
3455    for feature in value.split(' ') {
3456        validate_protocol_v2_token("fetch feature", feature)?;
3457        match feature {
3458            "shallow" => out.shallow = true,
3459            "wait-for-done" => out.wait_for_done = true,
3460            "filter" => out.filter = true,
3461            "ref-in-want" => out.ref_in_want = true,
3462            "sideband-all" => out.sideband_all = true,
3463            "packfile-uris" => out.packfile_uris = true,
3464            other => out.unknown.push(other.to_string()),
3465        }
3466    }
3467    Ok(out)
3468}
3469
3470pub fn parse_capabilities(input: &[u8]) -> Result<Vec<Capability>> {
3471    let input = trim_trailing_lf(input);
3472    if input.is_empty() {
3473        return Ok(Vec::new());
3474    }
3475    let text =
3476        std::str::from_utf8(input).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
3477    text.split(' ')
3478        .map(parse_capability_token)
3479        .collect::<Result<Vec<_>>>()
3480}
3481
3482pub fn encode_capabilities(capabilities: &[Capability]) -> Result<Vec<u8>> {
3483    let mut out = Vec::new();
3484    for (idx, capability) in capabilities.iter().enumerate() {
3485        validate_capability_field("capability name", &capability.name)?;
3486        if idx != 0 {
3487            out.push(b' ');
3488        }
3489        out.extend_from_slice(capability.name.as_bytes());
3490        if let Some(value) = &capability.value {
3491            validate_capability_field("capability value", value)?;
3492            out.push(b'=');
3493            out.extend_from_slice(value.as_bytes());
3494        }
3495    }
3496    Ok(out)
3497}
3498
3499pub fn parse_ref_advertisement(format: ObjectFormat, payload: &[u8]) -> Result<RefAdvertisement> {
3500    let payload = trim_trailing_lf(payload);
3501    let (reference, capabilities) = match payload.iter().position(|byte| *byte == 0) {
3502        Some(idx) => (&payload[..idx], parse_capabilities(&payload[idx + 1..])?),
3503        None => (payload, Vec::new()),
3504    };
3505    let text =
3506        std::str::from_utf8(reference).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
3507    let (oid, name) = text
3508        .split_once(' ')
3509        .ok_or_else(|| GitError::InvalidFormat("advertised ref is missing name".into()))?;
3510    if name.is_empty() {
3511        return Err(GitError::InvalidFormat(
3512            "advertised ref name is empty".into(),
3513        ));
3514    }
3515    Ok(RefAdvertisement {
3516        oid: ObjectId::from_hex(format, oid)?,
3517        name: name.to_string(),
3518        capabilities,
3519    })
3520}
3521
3522pub fn encode_ref_advertisement(advertisement: &RefAdvertisement) -> Result<Vec<u8>> {
3523    validate_protocol_v2_token("advertised ref name", &advertisement.name)?;
3524    let mut out = advertisement.oid.to_string().into_bytes();
3525    out.push(b' ');
3526    out.extend_from_slice(advertisement.name.as_bytes());
3527    if !advertisement.capabilities.is_empty() {
3528        out.push(0);
3529        out.extend_from_slice(&encode_capabilities(&advertisement.capabilities)?);
3530    }
3531    out.push(b'\n');
3532    Ok(out)
3533}
3534
3535pub fn parse_ref_advertisements(
3536    format: ObjectFormat,
3537    frames: &[PktLineFrame],
3538) -> Result<Vec<RefAdvertisement>> {
3539    Ok(parse_ref_advertisement_set(format, frames)?.refs)
3540}
3541
3542pub fn parse_ref_advertisement_set(
3543    format: ObjectFormat,
3544    frames: &[PktLineFrame],
3545) -> Result<RefAdvertisementSet> {
3546    let mut set = RefAdvertisementSet {
3547        protocol: ProtocolVersion::V0,
3548        refs: Vec::new(),
3549        shallow: Vec::new(),
3550    };
3551    let mut saw_flush = false;
3552    let mut in_shallow = false;
3553    for (idx, frame) in frames.iter().enumerate() {
3554        match frame {
3555            PktLineFrame::Data(payload) if !saw_flush => {
3556                let trimmed = trim_trailing_lf(payload);
3557                if trimmed == b"version 1" {
3558                    if idx != 0 {
3559                        return Err(GitError::InvalidFormat(
3560                            "advertised ref protocol version must be the first line".into(),
3561                        ));
3562                    }
3563                    set.protocol = ProtocolVersion::V1;
3564                    continue;
3565                }
3566                if trimmed.starts_with(b"version ") {
3567                    return Err(GitError::InvalidFormat(
3568                        "unsupported advertised ref protocol version".into(),
3569                    ));
3570                }
3571                if trimmed.starts_with(b"shallow ") {
3572                    if set.refs.is_empty() {
3573                        return Err(GitError::InvalidFormat(
3574                            "advertised shallow refs must follow advertised refs".into(),
3575                        ));
3576                    }
3577                    let text = parse_protocol_v2_line_text("advertised shallow ref", payload)?;
3578                    set.shallow.push(parse_oid_argument(
3579                        format,
3580                        "advertised shallow ref",
3581                        text,
3582                        "shallow ",
3583                    )?);
3584                    in_shallow = true;
3585                    continue;
3586                }
3587                if in_shallow {
3588                    return Err(GitError::InvalidFormat(
3589                        "advertised refs must not follow shallow refs".into(),
3590                    ));
3591                }
3592                let advertisement = parse_ref_advertisement(format, payload)?;
3593                if !set.refs.is_empty() && !advertisement.capabilities.is_empty() {
3594                    return Err(GitError::InvalidFormat(
3595                        "advertised ref capabilities must appear on the first ref".into(),
3596                    ));
3597                }
3598                set.refs.push(advertisement);
3599            }
3600            PktLineFrame::Data(_) => {
3601                return Err(GitError::InvalidFormat(
3602                    "advertised ref stream has data after flush".into(),
3603                ));
3604            }
3605            PktLineFrame::Flush => {
3606                saw_flush = true;
3607                if idx + 1 != frames.len() {
3608                    return Err(GitError::InvalidFormat(
3609                        "advertised ref stream has frames after flush".into(),
3610                    ));
3611                }
3612            }
3613            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
3614                return Err(GitError::InvalidFormat(
3615                    "advertised ref stream contains a non-flush control packet".into(),
3616                ));
3617            }
3618        }
3619    }
3620    if !saw_flush {
3621        return Err(GitError::InvalidFormat(
3622            "advertised ref stream missing flush".into(),
3623        ));
3624    }
3625    Ok(set)
3626}
3627
3628pub fn encode_ref_advertisements(advertisements: &[RefAdvertisement]) -> Result<Vec<PktLineFrame>> {
3629    encode_ref_advertisement_set(&RefAdvertisementSet {
3630        protocol: ProtocolVersion::V0,
3631        refs: advertisements.to_vec(),
3632        shallow: Vec::new(),
3633    })
3634}
3635
3636pub fn encode_ref_advertisement_set(set: &RefAdvertisementSet) -> Result<Vec<PktLineFrame>> {
3637    let mut frames = Vec::new();
3638    match set.protocol {
3639        ProtocolVersion::V0 => {}
3640        ProtocolVersion::V1 => frames.push(PktLineFrame::data(line_from_str("version 1"))?),
3641        ProtocolVersion::V2 => {
3642            return Err(GitError::InvalidFormat(
3643                "protocol v2 does not use v0/v1 advertised-ref streams".into(),
3644            ));
3645        }
3646    }
3647    if set.refs.is_empty() && !set.shallow.is_empty() {
3648        return Err(GitError::InvalidFormat(
3649            "advertised shallow refs require advertised refs".into(),
3650        ));
3651    }
3652    for (idx, advertisement) in set.refs.iter().enumerate() {
3653        if idx != 0 && !advertisement.capabilities.is_empty() {
3654            return Err(GitError::InvalidFormat(
3655                "advertised ref capabilities must appear on the first ref".into(),
3656            ));
3657        }
3658        frames.push(PktLineFrame::data(encode_ref_advertisement(
3659            advertisement,
3660        )?)?);
3661    }
3662    for oid in &set.shallow {
3663        frames.push(PktLineFrame::data(line_from_str(&format!(
3664            "shallow {oid}"
3665        )))?);
3666    }
3667    frames.push(PktLineFrame::Flush);
3668    Ok(frames)
3669}
3670
3671pub fn read_ref_advertisements(
3672    format: ObjectFormat,
3673    reader: &mut impl Read,
3674) -> Result<Vec<RefAdvertisement>> {
3675    let frames = read_pkt_line_frames_until_flush(reader)?;
3676    parse_ref_advertisements(format, &frames)
3677}
3678
3679pub fn read_ref_advertisement_set(
3680    format: ObjectFormat,
3681    reader: &mut impl Read,
3682) -> Result<RefAdvertisementSet> {
3683    let frames = read_pkt_line_frames_until_flush(reader)?;
3684    parse_ref_advertisement_set(format, &frames)
3685}
3686
3687pub fn write_ref_advertisements(
3688    writer: &mut impl Write,
3689    advertisements: &[RefAdvertisement],
3690) -> Result<()> {
3691    write_ref_advertisement_stream(writer, ProtocolVersion::V0, advertisements, &[])
3692}
3693
3694pub fn write_ref_advertisement_set(
3695    writer: &mut impl Write,
3696    set: &RefAdvertisementSet,
3697) -> Result<()> {
3698    write_ref_advertisement_stream(writer, set.protocol, &set.refs, &set.shallow)
3699}
3700
3701fn write_ref_advertisement_stream(
3702    writer: &mut impl Write,
3703    protocol: ProtocolVersion,
3704    refs: &[RefAdvertisement],
3705    shallow: &[ObjectId],
3706) -> Result<()> {
3707    match protocol {
3708        ProtocolVersion::V0 => {}
3709        ProtocolVersion::V1 => write_pkt_line_payload(writer, b"version 1\n")?,
3710        ProtocolVersion::V2 => {
3711            return Err(GitError::InvalidFormat(
3712                "protocol v2 does not use v0/v1 advertised-ref streams".into(),
3713            ));
3714        }
3715    }
3716    if refs.is_empty() && !shallow.is_empty() {
3717        return Err(GitError::InvalidFormat(
3718            "advertised shallow refs require advertised refs".into(),
3719        ));
3720    }
3721    for (idx, advertisement) in refs.iter().enumerate() {
3722        if idx != 0 && !advertisement.capabilities.is_empty() {
3723            return Err(GitError::InvalidFormat(
3724                "advertised ref capabilities must appear on the first ref".into(),
3725            ));
3726        }
3727        write_pkt_line_payload(writer, &encode_ref_advertisement(advertisement)?)?;
3728    }
3729    for oid in shallow {
3730        write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
3731    }
3732    writer.write_all(b"0000")?;
3733    Ok(())
3734}
3735
3736pub fn parse_dumb_http_info_refs(
3737    format: ObjectFormat,
3738    input: &[u8],
3739) -> Result<Vec<DumbHttpRefRecord>> {
3740    if input.is_empty() {
3741        return Ok(Vec::new());
3742    }
3743    input
3744        .split_inclusive(|byte| *byte == b'\n')
3745        .map(|line| parse_dumb_http_info_ref_record(format, line))
3746        .collect()
3747}
3748
3749pub fn encode_dumb_http_info_refs(records: &[DumbHttpRefRecord]) -> Result<Vec<u8>> {
3750    let mut out = Vec::new();
3751    for record in records {
3752        validate_dumb_http_ref_name(&record.name)?;
3753        out.extend_from_slice(record.oid.to_string().as_bytes());
3754        out.push(b'\t');
3755        out.extend_from_slice(record.name.as_bytes());
3756        if record.peeled {
3757            out.extend_from_slice(b"^{}");
3758        }
3759        out.push(b'\n');
3760    }
3761    Ok(out)
3762}
3763
3764pub fn read_dumb_http_info_refs(
3765    format: ObjectFormat,
3766    reader: &mut impl Read,
3767) -> Result<Vec<DumbHttpRefRecord>> {
3768    let mut input = Vec::new();
3769    reader.read_to_end(&mut input)?;
3770    parse_dumb_http_info_refs(format, &input)
3771}
3772
3773pub fn write_dumb_http_info_refs(
3774    writer: &mut impl Write,
3775    records: &[DumbHttpRefRecord],
3776) -> Result<()> {
3777    for record in records {
3778        validate_dumb_http_ref_name(&record.name)?;
3779        writer.write_all(record.oid.to_string().as_bytes())?;
3780        writer.write_all(b"\t")?;
3781        writer.write_all(record.name.as_bytes())?;
3782        if record.peeled {
3783            writer.write_all(b"^{}")?;
3784        }
3785        writer.write_all(b"\n")?;
3786    }
3787    Ok(())
3788}
3789
3790pub fn parse_dumb_http_alternates(input: &[u8]) -> Result<Vec<String>> {
3791    if input.is_empty() {
3792        return Ok(Vec::new());
3793    }
3794    input
3795        .split_inclusive(|byte| *byte == b'\n')
3796        .map(parse_dumb_http_alternate)
3797        .collect()
3798}
3799
3800pub fn encode_dumb_http_alternates(alternates: &[String]) -> Result<Vec<u8>> {
3801    let mut out = Vec::new();
3802    for alternate in alternates {
3803        validate_dumb_http_alternate(alternate)?;
3804        out.extend_from_slice(alternate.as_bytes());
3805        out.push(b'\n');
3806    }
3807    Ok(out)
3808}
3809
3810pub fn read_dumb_http_alternates(reader: &mut impl Read) -> Result<Vec<String>> {
3811    let mut input = Vec::new();
3812    reader.read_to_end(&mut input)?;
3813    parse_dumb_http_alternates(&input)
3814}
3815
3816pub fn write_dumb_http_alternates(writer: &mut impl Write, alternates: &[String]) -> Result<()> {
3817    for alternate in alternates {
3818        validate_dumb_http_alternate(alternate)?;
3819        writer.write_all(alternate.as_bytes())?;
3820        writer.write_all(b"\n")?;
3821    }
3822    Ok(())
3823}
3824
3825pub fn parse_dumb_http_packs(
3826    format: ObjectFormat,
3827    input: &[u8],
3828) -> Result<Vec<DumbHttpPackRecord>> {
3829    if input.is_empty() {
3830        return Ok(Vec::new());
3831    }
3832    input
3833        .split_inclusive(|byte| *byte == b'\n')
3834        .map(|line| parse_dumb_http_pack_record(format, line))
3835        .collect()
3836}
3837
3838pub fn encode_dumb_http_packs(records: &[DumbHttpPackRecord]) -> Result<Vec<u8>> {
3839    let mut out = Vec::new();
3840    for record in records {
3841        out.extend_from_slice(format!("P pack-{}.pack\n", record.hash).as_bytes());
3842    }
3843    Ok(out)
3844}
3845
3846pub fn read_dumb_http_packs(
3847    format: ObjectFormat,
3848    reader: &mut impl Read,
3849) -> Result<Vec<DumbHttpPackRecord>> {
3850    let mut input = Vec::new();
3851    reader.read_to_end(&mut input)?;
3852    parse_dumb_http_packs(format, &input)
3853}
3854
3855pub fn write_dumb_http_packs(
3856    writer: &mut impl Write,
3857    records: &[DumbHttpPackRecord],
3858) -> Result<()> {
3859    for record in records {
3860        writer.write_all(format!("P pack-{}.pack\n", record.hash).as_bytes())?;
3861    }
3862    Ok(())
3863}
3864
3865pub fn parse_upload_pack_request(
3866    format: ObjectFormat,
3867    frames: &[PktLineFrame],
3868) -> Result<Option<UploadPackRequest>> {
3869    if matches!(frames, [PktLineFrame::Flush]) {
3870        return Ok(None);
3871    }
3872
3873    let mut request = UploadPackRequest::default();
3874    let mut in_options = false;
3875    let mut saw_flush = false;
3876    for (idx, frame) in frames.iter().enumerate() {
3877        match frame {
3878            PktLineFrame::Data(payload) if !saw_flush => {
3879                let text = parse_protocol_v2_line_text("upload-pack request line", payload)?;
3880                if let Some(value) = text.strip_prefix("want ") {
3881                    if in_options {
3882                        return Err(GitError::InvalidFormat(
3883                            "upload-pack request has want after options".into(),
3884                        ));
3885                    }
3886                    let (oid, capabilities) = if request.wants.is_empty() {
3887                        value
3888                            .split_once(' ')
3889                            .map_or((value, None), |(oid, caps)| (oid, Some(caps.as_bytes())))
3890                    } else {
3891                        if value.contains(' ') {
3892                            return Err(GitError::InvalidFormat(
3893                                "additional upload-pack want has capabilities".into(),
3894                            ));
3895                        }
3896                        (value, None)
3897                    };
3898                    validate_protocol_v2_token("upload-pack want", oid)?;
3899                    request.wants.push(ObjectId::from_hex(format, oid)?);
3900                    if let Some(capabilities) = capabilities {
3901                        request.capabilities = parse_capabilities(capabilities)?;
3902                    }
3903                    continue;
3904                }
3905
3906                if request.wants.is_empty() {
3907                    return Err(GitError::InvalidFormat(
3908                        "upload-pack request must start with want".into(),
3909                    ));
3910                }
3911                in_options = true;
3912                if text.starts_with("shallow ") {
3913                    request.shallow.push(parse_oid_argument(
3914                        format,
3915                        "upload-pack shallow",
3916                        text,
3917                        "shallow ",
3918                    )?);
3919                } else if text.starts_with("deepen ") {
3920                    if request.deepen.is_some() {
3921                        return Err(GitError::InvalidFormat(
3922                            "upload-pack request has duplicate deepen".into(),
3923                        ));
3924                    }
3925                    request.deepen =
3926                        Some(parse_u32_argument("upload-pack deepen", text, "deepen ")?);
3927                } else if text.starts_with("deepen-since ") {
3928                    if request.deepen_since.is_some() {
3929                        return Err(GitError::InvalidFormat(
3930                            "upload-pack request has duplicate deepen-since".into(),
3931                        ));
3932                    }
3933                    request.deepen_since = Some(parse_u64_argument(
3934                        "upload-pack deepen-since",
3935                        text,
3936                        "deepen-since ",
3937                    )?);
3938                } else if let Some(name) = text.strip_prefix("deepen-not ") {
3939                    validate_protocol_v2_token("upload-pack deepen-not", name)?;
3940                    request.deepen_not.push(name.to_string());
3941                } else if let Some(filter) = text.strip_prefix("filter ") {
3942                    if request.filter.is_some() {
3943                        return Err(GitError::InvalidFormat(
3944                            "upload-pack request has duplicate filter".into(),
3945                        ));
3946                    }
3947                    validate_protocol_v2_token("upload-pack filter", filter)?;
3948                    request.filter = Some(filter.to_string());
3949                } else {
3950                    return Err(GitError::InvalidFormat(format!(
3951                        "unsupported upload-pack request line {text}"
3952                    )));
3953                }
3954            }
3955            PktLineFrame::Data(_) => {
3956                return Err(GitError::InvalidFormat(
3957                    "upload-pack request has data after flush".into(),
3958                ));
3959            }
3960            PktLineFrame::Flush => {
3961                saw_flush = true;
3962                if idx + 1 != frames.len() {
3963                    return Err(GitError::InvalidFormat(
3964                        "upload-pack request has frames after flush".into(),
3965                    ));
3966                }
3967            }
3968            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
3969                return Err(GitError::InvalidFormat(
3970                    "upload-pack request contains a non-flush control packet".into(),
3971                ));
3972            }
3973        }
3974    }
3975    if !saw_flush {
3976        return Err(GitError::InvalidFormat(
3977            "upload-pack request missing flush".into(),
3978        ));
3979    }
3980    if request.wants.is_empty() {
3981        return Err(GitError::InvalidFormat(
3982            "upload-pack request missing want".into(),
3983        ));
3984    }
3985    Ok(Some(request))
3986}
3987
3988pub fn encode_upload_pack_request(
3989    request: Option<&UploadPackRequest>,
3990) -> Result<Vec<PktLineFrame>> {
3991    let Some(request) = request else {
3992        return Ok(vec![PktLineFrame::Flush]);
3993    };
3994    if request.wants.is_empty() {
3995        return Err(GitError::InvalidFormat(
3996            "upload-pack request missing want".into(),
3997        ));
3998    }
3999
4000    let mut frames = Vec::new();
4001    for (idx, oid) in request.wants.iter().enumerate() {
4002        let mut line = format!("want {oid}");
4003        if idx == 0 && !request.capabilities.is_empty() {
4004            line.push(' ');
4005            line.push_str(
4006                &String::from_utf8(encode_capabilities(&request.capabilities)?)
4007                    .map_err(|err| GitError::InvalidFormat(err.to_string()))?,
4008            );
4009        }
4010        frames.push(PktLineFrame::data(line_from_str(&line))?);
4011    }
4012    for oid in &request.shallow {
4013        frames.push(PktLineFrame::data(line_from_str(&format!(
4014            "shallow {oid}"
4015        )))?);
4016    }
4017    if let Some(deepen) = request.deepen {
4018        if deepen == 0 {
4019            return Err(GitError::InvalidFormat(
4020                "upload-pack deepen must be positive".into(),
4021            ));
4022        }
4023        frames.push(PktLineFrame::data(line_from_str(&format!(
4024            "deepen {deepen}"
4025        )))?);
4026    }
4027    if let Some(deepen_since) = request.deepen_since {
4028        frames.push(PktLineFrame::data(line_from_str(&format!(
4029            "deepen-since {deepen_since}"
4030        )))?);
4031    }
4032    for name in &request.deepen_not {
4033        validate_protocol_v2_token("upload-pack deepen-not", name)?;
4034        frames.push(PktLineFrame::data(line_from_str(&format!(
4035            "deepen-not {name}"
4036        )))?);
4037    }
4038    if let Some(filter) = &request.filter {
4039        validate_protocol_v2_token("upload-pack filter", filter)?;
4040        frames.push(PktLineFrame::data(line_from_str(&format!(
4041            "filter {filter}"
4042        )))?);
4043    }
4044    frames.push(PktLineFrame::Flush);
4045    Ok(frames)
4046}
4047
4048pub fn read_upload_pack_request(
4049    format: ObjectFormat,
4050    reader: &mut impl Read,
4051) -> Result<Option<UploadPackRequest>> {
4052    let frames = read_pkt_line_frames_until_flush(reader)?;
4053    parse_upload_pack_request(format, &frames)
4054}
4055
4056pub fn write_upload_pack_request(
4057    writer: &mut impl Write,
4058    request: Option<&UploadPackRequest>,
4059) -> Result<()> {
4060    let Some(request) = request else {
4061        writer.write_all(b"0000")?;
4062        return Ok(());
4063    };
4064    if request.wants.is_empty() {
4065        return Err(GitError::InvalidFormat(
4066            "upload-pack request missing want".into(),
4067        ));
4068    }
4069
4070    for (idx, oid) in request.wants.iter().enumerate() {
4071        let mut line = format!("want {oid}");
4072        if idx == 0 && !request.capabilities.is_empty() {
4073            line.push(' ');
4074            line.push_str(
4075                &String::from_utf8(encode_capabilities(&request.capabilities)?)
4076                    .map_err(|err| GitError::InvalidFormat(err.to_string()))?,
4077            );
4078        }
4079        write_pkt_line_payload(writer, &line_from_str(&line))?;
4080    }
4081    for oid in &request.shallow {
4082        write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
4083    }
4084    if let Some(deepen) = request.deepen {
4085        if deepen == 0 {
4086            return Err(GitError::InvalidFormat(
4087                "upload-pack deepen must be positive".into(),
4088            ));
4089        }
4090        write_pkt_line_payload(writer, &line_from_str(&format!("deepen {deepen}")))?;
4091    }
4092    if let Some(deepen_since) = request.deepen_since {
4093        write_pkt_line_payload(
4094            writer,
4095            &line_from_str(&format!("deepen-since {deepen_since}")),
4096        )?;
4097    }
4098    for name in &request.deepen_not {
4099        validate_protocol_v2_token("upload-pack deepen-not", name)?;
4100        write_pkt_line_payload(writer, &line_from_str(&format!("deepen-not {name}")))?;
4101    }
4102    if let Some(filter) = &request.filter {
4103        validate_protocol_v2_token("upload-pack filter", filter)?;
4104        write_pkt_line_payload(writer, &line_from_str(&format!("filter {filter}")))?;
4105    }
4106    writer.write_all(b"0000")?;
4107    Ok(())
4108}
4109
4110pub fn parse_upload_pack_features(capabilities: &[Capability]) -> Result<UploadPackFeatures> {
4111    let mut features = UploadPackFeatures::default();
4112    for capability in capabilities {
4113        match capability.name.as_str() {
4114            "multi_ack" => set_upload_pack_flag(&mut features.multi_ack, capability)?,
4115            "multi_ack_detailed" => {
4116                set_upload_pack_flag(&mut features.multi_ack_detailed, capability)?
4117            }
4118            "no-done" => set_upload_pack_flag(&mut features.no_done, capability)?,
4119            "thin-pack" => set_upload_pack_flag(&mut features.thin_pack, capability)?,
4120            "side-band" => set_upload_pack_flag(&mut features.side_band, capability)?,
4121            "side-band-64k" => set_upload_pack_flag(&mut features.side_band_64k, capability)?,
4122            "ofs-delta" => set_upload_pack_flag(&mut features.ofs_delta, capability)?,
4123            "shallow" => set_upload_pack_flag(&mut features.shallow, capability)?,
4124            "deepen-since" => set_upload_pack_flag(&mut features.deepen_since, capability)?,
4125            "deepen-not" => set_upload_pack_flag(&mut features.deepen_not, capability)?,
4126            "include-tag" => set_upload_pack_flag(&mut features.include_tag, capability)?,
4127            "no-progress" => set_upload_pack_flag(&mut features.no_progress, capability)?,
4128            "allow-tip-sha1-in-want" => {
4129                set_upload_pack_flag(&mut features.allow_tip_sha1_in_want, capability)?
4130            }
4131            "allow-reachable-sha1-in-want" => {
4132                set_upload_pack_flag(&mut features.allow_reachable_sha1_in_want, capability)?
4133            }
4134            "filter" => set_upload_pack_flag(&mut features.filter, capability)?,
4135            "agent" => {
4136                let Some(agent) = &capability.value else {
4137                    return Err(GitError::InvalidFormat(
4138                        "upload-pack agent capability is missing value".into(),
4139                    ));
4140                };
4141                if features.agent.is_some() {
4142                    return Err(GitError::InvalidFormat(
4143                        "upload-pack has duplicate agent capability".into(),
4144                    ));
4145                }
4146                validate_capability_field("upload-pack agent", agent)?;
4147                features.agent = Some(agent.clone());
4148            }
4149            "object-format" => {
4150                let Some(format) = &capability.value else {
4151                    return Err(GitError::InvalidFormat(
4152                        "upload-pack object-format capability is missing value".into(),
4153                    ));
4154                };
4155                if features.object_format.is_some() {
4156                    return Err(GitError::InvalidFormat(
4157                        "upload-pack has duplicate object-format capability".into(),
4158                    ));
4159                }
4160                validate_capability_field("upload-pack object-format", format)?;
4161                features.object_format = Some(format.parse()?);
4162            }
4163            "symref" => {
4164                let Some(symref) = &capability.value else {
4165                    return Err(GitError::InvalidFormat(
4166                        "upload-pack symref capability is missing value".into(),
4167                    ));
4168                };
4169                validate_capability_field("upload-pack symref", symref)?;
4170                features.symrefs.push(symref.clone());
4171            }
4172            _ => {
4173                encode_capabilities(std::slice::from_ref(capability))?;
4174                if features
4175                    .unknown
4176                    .iter()
4177                    .any(|known| known.name == capability.name)
4178                {
4179                    return Err(GitError::InvalidFormat(format!(
4180                        "upload-pack has duplicate {} capability",
4181                        capability.name
4182                    )));
4183                }
4184                features.unknown.push(capability.clone());
4185            }
4186        }
4187    }
4188    Ok(features)
4189}
4190
4191pub fn encode_upload_pack_features(features: &UploadPackFeatures) -> Result<Vec<Capability>> {
4192    let mut capabilities = Vec::new();
4193    push_upload_pack_flag(&mut capabilities, "multi_ack", features.multi_ack);
4194    push_upload_pack_flag(
4195        &mut capabilities,
4196        "multi_ack_detailed",
4197        features.multi_ack_detailed,
4198    );
4199    push_upload_pack_flag(&mut capabilities, "no-done", features.no_done);
4200    push_upload_pack_flag(&mut capabilities, "thin-pack", features.thin_pack);
4201    push_upload_pack_flag(&mut capabilities, "side-band", features.side_band);
4202    push_upload_pack_flag(&mut capabilities, "side-band-64k", features.side_band_64k);
4203    push_upload_pack_flag(&mut capabilities, "ofs-delta", features.ofs_delta);
4204    push_upload_pack_flag(&mut capabilities, "shallow", features.shallow);
4205    push_upload_pack_flag(&mut capabilities, "deepen-since", features.deepen_since);
4206    push_upload_pack_flag(&mut capabilities, "deepen-not", features.deepen_not);
4207    push_upload_pack_flag(&mut capabilities, "include-tag", features.include_tag);
4208    push_upload_pack_flag(&mut capabilities, "no-progress", features.no_progress);
4209    push_upload_pack_flag(
4210        &mut capabilities,
4211        "allow-tip-sha1-in-want",
4212        features.allow_tip_sha1_in_want,
4213    );
4214    push_upload_pack_flag(
4215        &mut capabilities,
4216        "allow-reachable-sha1-in-want",
4217        features.allow_reachable_sha1_in_want,
4218    );
4219    push_upload_pack_flag(&mut capabilities, "filter", features.filter);
4220    if let Some(agent) = &features.agent {
4221        validate_capability_field("upload-pack agent", agent)?;
4222        capabilities.push(Capability {
4223            name: "agent".into(),
4224            value: Some(agent.clone()),
4225        });
4226    }
4227    if let Some(format) = features.object_format {
4228        capabilities.push(Capability {
4229            name: "object-format".into(),
4230            value: Some(format.name().into()),
4231        });
4232    }
4233    for symref in &features.symrefs {
4234        validate_capability_field("upload-pack symref", symref)?;
4235        capabilities.push(Capability {
4236            name: "symref".into(),
4237            value: Some(symref.clone()),
4238        });
4239    }
4240    for capability in &features.unknown {
4241        if is_known_upload_pack_capability(&capability.name) {
4242            return Err(GitError::InvalidFormat(format!(
4243                "upload-pack unknown capability duplicates known capability {}",
4244                capability.name
4245            )));
4246        }
4247        encode_capabilities(std::slice::from_ref(capability))?;
4248        capabilities.push(capability.clone());
4249    }
4250    Ok(capabilities)
4251}
4252
4253pub fn validate_upload_pack_request_features(
4254    features: &UploadPackFeatures,
4255    request: &UploadPackRequest,
4256) -> Result<()> {
4257    for capability in &request.capabilities {
4258        if is_upload_pack_flag_capability(&capability.name) {
4259            reject_capability_value("upload-pack request capability", capability)?;
4260        }
4261        match capability.name.as_str() {
4262            "multi_ack" if !features.multi_ack => {
4263                return Err(GitError::InvalidFormat(
4264                    "upload-pack request uses multi_ack without advertised capability".into(),
4265                ));
4266            }
4267            "multi_ack_detailed" if !features.multi_ack_detailed => {
4268                return Err(GitError::InvalidFormat(
4269                    "upload-pack request uses multi_ack_detailed without advertised capability"
4270                        .into(),
4271                ));
4272            }
4273            "no-done" if !features.no_done => {
4274                return Err(GitError::InvalidFormat(
4275                    "upload-pack request uses no-done without advertised capability".into(),
4276                ));
4277            }
4278            "thin-pack" if !features.thin_pack => {
4279                return Err(GitError::InvalidFormat(
4280                    "upload-pack request uses thin-pack without advertised capability".into(),
4281                ));
4282            }
4283            "side-band" if !features.side_band => {
4284                return Err(GitError::InvalidFormat(
4285                    "upload-pack request uses side-band without advertised capability".into(),
4286                ));
4287            }
4288            "side-band-64k" if !features.side_band_64k => {
4289                return Err(GitError::InvalidFormat(
4290                    "upload-pack request uses side-band-64k without advertised capability".into(),
4291                ));
4292            }
4293            "ofs-delta" if !features.ofs_delta => {
4294                return Err(GitError::InvalidFormat(
4295                    "upload-pack request uses ofs-delta without advertised capability".into(),
4296                ));
4297            }
4298            "include-tag" if !features.include_tag => {
4299                return Err(GitError::InvalidFormat(
4300                    "upload-pack request uses include-tag without advertised capability".into(),
4301                ));
4302            }
4303            "no-progress" if !features.no_progress => {
4304                return Err(GitError::InvalidFormat(
4305                    "upload-pack request uses no-progress without advertised capability".into(),
4306                ));
4307            }
4308            "allow-tip-sha1-in-want" if !features.allow_tip_sha1_in_want => {
4309                return Err(GitError::InvalidFormat(
4310                    "upload-pack request uses allow-tip-sha1-in-want without advertised capability"
4311                        .into(),
4312                ));
4313            }
4314            "allow-reachable-sha1-in-want" if !features.allow_reachable_sha1_in_want => {
4315                return Err(GitError::InvalidFormat(
4316                    "upload-pack request uses allow-reachable-sha1-in-want without advertised capability"
4317                        .into(),
4318                ));
4319            }
4320            "filter" if !features.filter => {
4321                return Err(GitError::InvalidFormat(
4322                    "upload-pack request uses filter capability without advertised capability"
4323                        .into(),
4324                ));
4325            }
4326            "agent" => {
4327                let Some(agent) = &capability.value else {
4328                    return Err(GitError::InvalidFormat(
4329                        "upload-pack request agent capability is missing value".into(),
4330                    ));
4331                };
4332                validate_capability_field("upload-pack request agent", agent)?;
4333            }
4334            "object-format" => {
4335                let Some(format) = &capability.value else {
4336                    return Err(GitError::InvalidFormat(
4337                        "upload-pack request object-format capability is missing value".into(),
4338                    ));
4339                };
4340                let requested_format: ObjectFormat = format.parse()?;
4341                if features.object_format != Some(requested_format) {
4342                    return Err(GitError::InvalidFormat(
4343                        "upload-pack request object-format was not advertised".into(),
4344                    ));
4345                }
4346            }
4347            name if is_known_upload_pack_capability(name) => {}
4348            _ => {
4349                if !features
4350                    .unknown
4351                    .iter()
4352                    .any(|advertised| advertised.name == capability.name)
4353                {
4354                    return Err(GitError::InvalidFormat(format!(
4355                        "upload-pack request uses unadvertised capability {}",
4356                        capability.name
4357                    )));
4358                }
4359            }
4360        }
4361    }
4362
4363    let sideband = request
4364        .capabilities
4365        .iter()
4366        .any(|capability| capability.name == "side-band");
4367    let sideband_64k = request
4368        .capabilities
4369        .iter()
4370        .any(|capability| capability.name == "side-band-64k");
4371    if sideband && sideband_64k {
4372        return Err(GitError::InvalidFormat(
4373            "upload-pack request must not request both side-band and side-band-64k".into(),
4374        ));
4375    }
4376
4377    if !features.shallow && (!request.shallow.is_empty() || request.deepen.is_some()) {
4378        return Err(GitError::InvalidFormat(
4379            "upload-pack request uses shallow/deepen without advertised shallow capability".into(),
4380        ));
4381    }
4382    if !features.deepen_since && request.deepen_since.is_some() {
4383        return Err(GitError::InvalidFormat(
4384            "upload-pack request uses deepen-since without advertised capability".into(),
4385        ));
4386    }
4387    if !features.deepen_not && !request.deepen_not.is_empty() {
4388        return Err(GitError::InvalidFormat(
4389            "upload-pack request uses deepen-not without advertised capability".into(),
4390        ));
4391    }
4392    if !features.filter && request.filter.is_some() {
4393        return Err(GitError::InvalidFormat(
4394            "upload-pack request uses filter without advertised capability".into(),
4395        ));
4396    }
4397    Ok(())
4398}
4399
4400pub fn build_upload_pack_raw_packfile_response<C, B>(
4401    features: &UploadPackFeatures,
4402    request: UploadPackRequest,
4403    haves: impl IntoIterator<Item = ObjectId>,
4404    mut contains_object: C,
4405    mut build_pack: B,
4406) -> Result<UploadPackRawPackfileResponse>
4407where
4408    C: FnMut(&ObjectId) -> Result<bool>,
4409    B: FnMut(Vec<ObjectId>, Vec<ObjectId>) -> Result<Option<Vec<u8>>>,
4410{
4411    validate_upload_pack_request_features(features, &request)?;
4412    for want in &request.wants {
4413        if !contains_object(want)? {
4414            return Err(GitError::InvalidObject(format!(
4415                "upload-pack requested missing object {want}"
4416            )));
4417        }
4418    }
4419    let known_haves = haves
4420        .into_iter()
4421        .filter_map(|oid| match contains_object(&oid) {
4422            Ok(true) => Some(Ok(oid)),
4423            Ok(false) => None,
4424            Err(err) => Some(Err(err)),
4425        })
4426        .collect::<Result<Vec<_>>>()?;
4427    let packfile = build_pack(request.wants, known_haves)?
4428        .ok_or_else(|| GitError::InvalidObject("upload-pack request produced empty pack".into()))?;
4429    Ok(UploadPackRawPackfileResponse {
4430        acknowledgments: vec![UploadPackAcknowledgment::Nak],
4431        packfile,
4432    })
4433}
4434
4435pub fn parse_upload_pack_shallow_update(
4436    format: ObjectFormat,
4437    frames: &[PktLineFrame],
4438) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
4439    let mut entries = Vec::new();
4440    let mut saw_flush = false;
4441    for (idx, frame) in frames.iter().enumerate() {
4442        match frame {
4443            PktLineFrame::Data(payload) if !saw_flush => {
4444                entries.push(parse_fetch_shallow_info(format, payload)?);
4445            }
4446            PktLineFrame::Data(_) => {
4447                return Err(GitError::InvalidFormat(
4448                    "upload-pack shallow update has data after flush".into(),
4449                ));
4450            }
4451            PktLineFrame::Flush => {
4452                saw_flush = true;
4453                if idx + 1 != frames.len() {
4454                    return Err(GitError::InvalidFormat(
4455                        "upload-pack shallow update has frames after flush".into(),
4456                    ));
4457                }
4458            }
4459            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4460                return Err(GitError::InvalidFormat(
4461                    "upload-pack shallow update contains a non-flush control packet".into(),
4462                ));
4463            }
4464        }
4465    }
4466    if !saw_flush {
4467        return Err(GitError::InvalidFormat(
4468            "upload-pack shallow update missing flush".into(),
4469        ));
4470    }
4471    Ok(entries)
4472}
4473
4474pub fn encode_upload_pack_shallow_update(
4475    entries: &[ProtocolV2FetchShallowInfo],
4476) -> Result<Vec<PktLineFrame>> {
4477    let mut frames = Vec::new();
4478    for entry in entries {
4479        let line = match entry {
4480            ProtocolV2FetchShallowInfo::Shallow(oid) => format!("shallow {oid}"),
4481            ProtocolV2FetchShallowInfo::Unshallow(oid) => format!("unshallow {oid}"),
4482        };
4483        frames.push(PktLineFrame::data(line_from_str(&line))?);
4484    }
4485    frames.push(PktLineFrame::Flush);
4486    Ok(frames)
4487}
4488
4489pub fn read_upload_pack_shallow_update(
4490    format: ObjectFormat,
4491    reader: &mut impl Read,
4492) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
4493    let frames = read_pkt_line_frames_until_flush(reader)?;
4494    parse_upload_pack_shallow_update(format, &frames)
4495}
4496
4497pub fn write_upload_pack_shallow_update(
4498    writer: &mut impl Write,
4499    entries: &[ProtocolV2FetchShallowInfo],
4500) -> Result<()> {
4501    for entry in entries {
4502        let line = match entry {
4503            ProtocolV2FetchShallowInfo::Shallow(oid) => format!("shallow {oid}"),
4504            ProtocolV2FetchShallowInfo::Unshallow(oid) => format!("unshallow {oid}"),
4505        };
4506        write_pkt_line_payload(writer, &line_from_str(&line))?;
4507    }
4508    writer.write_all(b"0000")?;
4509    Ok(())
4510}
4511
4512pub fn parse_upload_pack_negotiation_request(
4513    format: ObjectFormat,
4514    frames: &[PktLineFrame],
4515) -> Result<UploadPackNegotiationRequest> {
4516    let mut request = UploadPackNegotiationRequest::default();
4517    let mut terminated = false;
4518    for (idx, frame) in frames.iter().enumerate() {
4519        match frame {
4520            PktLineFrame::Data(payload) if !terminated => {
4521                let text = parse_protocol_v2_line_text("upload-pack negotiation line", payload)?;
4522                if text == "done" {
4523                    request.done = true;
4524                    terminated = true;
4525                    if idx + 1 != frames.len() {
4526                        return Err(GitError::InvalidFormat(
4527                            "upload-pack negotiation has frames after done".into(),
4528                        ));
4529                    }
4530                } else if text.starts_with("have ") {
4531                    request.haves.push(parse_oid_argument(
4532                        format,
4533                        "upload-pack have",
4534                        text,
4535                        "have ",
4536                    )?);
4537                } else {
4538                    return Err(GitError::InvalidFormat(format!(
4539                        "unsupported upload-pack negotiation line {text}"
4540                    )));
4541                }
4542            }
4543            PktLineFrame::Data(_) => {
4544                return Err(GitError::InvalidFormat(
4545                    "upload-pack negotiation has data after terminator".into(),
4546                ));
4547            }
4548            PktLineFrame::Flush => {
4549                terminated = true;
4550                if idx + 1 != frames.len() {
4551                    return Err(GitError::InvalidFormat(
4552                        "upload-pack negotiation has frames after flush".into(),
4553                    ));
4554                }
4555            }
4556            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4557                return Err(GitError::InvalidFormat(
4558                    "upload-pack negotiation contains a non-flush control packet".into(),
4559                ));
4560            }
4561        }
4562    }
4563    if !terminated {
4564        return Err(GitError::InvalidFormat(
4565            "upload-pack negotiation missing terminator".into(),
4566        ));
4567    }
4568    Ok(request)
4569}
4570
4571pub fn encode_upload_pack_negotiation_request(
4572    request: &UploadPackNegotiationRequest,
4573) -> Result<Vec<PktLineFrame>> {
4574    let mut frames = Vec::new();
4575    for oid in &request.haves {
4576        frames.push(PktLineFrame::data(line_from_str(&format!("have {oid}")))?);
4577    }
4578    if request.done {
4579        frames.push(PktLineFrame::data(line_from_str("done"))?);
4580    } else {
4581        frames.push(PktLineFrame::Flush);
4582    }
4583    Ok(frames)
4584}
4585
4586pub fn read_upload_pack_negotiation_request(
4587    format: ObjectFormat,
4588    reader: &mut impl Read,
4589) -> Result<UploadPackNegotiationRequest> {
4590    let mut frames = Vec::new();
4591    loop {
4592        let Some(frame) = read_pkt_line_frame(reader)? else {
4593            return Err(GitError::InvalidFormat(
4594                "pkt-line stream ended before upload-pack negotiation terminator".into(),
4595            ));
4596        };
4597        let done = match &frame {
4598            PktLineFrame::Flush => true,
4599            PktLineFrame::Data(payload) => trim_trailing_lf(payload) == b"done",
4600            _ => false,
4601        };
4602        frames.push(frame);
4603        if done {
4604            return parse_upload_pack_negotiation_request(format, &frames);
4605        }
4606    }
4607}
4608
4609pub fn write_upload_pack_negotiation_request(
4610    writer: &mut impl Write,
4611    request: &UploadPackNegotiationRequest,
4612) -> Result<()> {
4613    for oid in &request.haves {
4614        write_pkt_line_payload(writer, &line_from_str(&format!("have {oid}")))?;
4615    }
4616    if request.done {
4617        write_pkt_line_payload(writer, b"done\n")?;
4618    } else {
4619        writer.write_all(b"0000")?;
4620    }
4621    Ok(())
4622}
4623
4624pub fn parse_upload_pack_acknowledgment(
4625    format: ObjectFormat,
4626    payload: &[u8],
4627) -> Result<UploadPackAcknowledgment> {
4628    let text = parse_protocol_v2_line_text("upload-pack acknowledgment", payload)?;
4629    if text == "NAK" {
4630        return Ok(UploadPackAcknowledgment::Nak);
4631    }
4632    let Some(rest) = text.strip_prefix("ACK ") else {
4633        return Err(GitError::InvalidFormat(format!(
4634            "unsupported upload-pack acknowledgment {text}"
4635        )));
4636    };
4637    let mut fields = rest.split(' ');
4638    let oid = fields
4639        .next()
4640        .ok_or_else(|| GitError::InvalidFormat("upload-pack ACK missing object id".into()))?;
4641    validate_protocol_v2_token("upload-pack ACK", oid)?;
4642    let status = match fields.next() {
4643        None => None,
4644        Some("continue") => Some(UploadPackAckStatus::Continue),
4645        Some("common") => Some(UploadPackAckStatus::Common),
4646        Some("ready") => Some(UploadPackAckStatus::Ready),
4647        Some(other) => {
4648            return Err(GitError::InvalidFormat(format!(
4649                "unsupported upload-pack ACK status {other}"
4650            )));
4651        }
4652    };
4653    if fields.next().is_some() {
4654        return Err(GitError::InvalidFormat(
4655            "upload-pack ACK has too many fields".into(),
4656        ));
4657    }
4658    Ok(UploadPackAcknowledgment::Ack {
4659        oid: ObjectId::from_hex(format, oid)?,
4660        status,
4661    })
4662}
4663
4664pub fn encode_upload_pack_acknowledgment(
4665    acknowledgment: &UploadPackAcknowledgment,
4666) -> Result<Vec<u8>> {
4667    let line = match acknowledgment {
4668        UploadPackAcknowledgment::Nak => "NAK".to_string(),
4669        UploadPackAcknowledgment::Ack { oid, status } => {
4670            let mut line = format!("ACK {oid}");
4671            if let Some(status) = status {
4672                line.push(' ');
4673                line.push_str(match status {
4674                    UploadPackAckStatus::Continue => "continue",
4675                    UploadPackAckStatus::Common => "common",
4676                    UploadPackAckStatus::Ready => "ready",
4677                });
4678            }
4679            line
4680        }
4681    };
4682    Ok(line_from_str(&line))
4683}
4684
4685pub fn read_upload_pack_acknowledgment(
4686    format: ObjectFormat,
4687    reader: &mut impl Read,
4688) -> Result<UploadPackAcknowledgment> {
4689    let Some(frame) = read_pkt_line_frame(reader)? else {
4690        return Err(GitError::InvalidFormat(
4691            "pkt-line stream ended before upload-pack acknowledgment".into(),
4692        ));
4693    };
4694    match frame {
4695        PktLineFrame::Data(payload) => parse_upload_pack_acknowledgment(format, &payload),
4696        _ => Err(GitError::InvalidFormat(
4697            "upload-pack acknowledgment must be a data packet".into(),
4698        )),
4699    }
4700}
4701
4702pub fn write_upload_pack_acknowledgment(
4703    writer: &mut impl Write,
4704    acknowledgment: &UploadPackAcknowledgment,
4705) -> Result<()> {
4706    write_pkt_line_payload(writer, &encode_upload_pack_acknowledgment(acknowledgment)?)
4707}
4708
4709pub fn parse_upload_pack_packfile_response(
4710    format: ObjectFormat,
4711    frames: &[PktLineFrame],
4712) -> Result<UploadPackPackfileResponse> {
4713    let mut response = UploadPackPackfileResponse::default();
4714    let mut in_sideband = false;
4715    let mut saw_flush = false;
4716    for (idx, frame) in frames.iter().enumerate() {
4717        match frame {
4718            PktLineFrame::Data(payload) if !saw_flush => {
4719                if !in_sideband
4720                    && (trim_trailing_lf(payload) == b"NAK" || payload.starts_with(b"ACK "))
4721                {
4722                    response
4723                        .acknowledgments
4724                        .push(parse_upload_pack_acknowledgment(format, payload)?);
4725                    continue;
4726                }
4727                in_sideband = true;
4728                response.sideband.push(parse_sideband_packet(payload)?);
4729            }
4730            PktLineFrame::Data(_) => {
4731                return Err(GitError::InvalidFormat(
4732                    "upload-pack packfile response has data after flush".into(),
4733                ));
4734            }
4735            PktLineFrame::Flush => {
4736                saw_flush = true;
4737                if idx + 1 != frames.len() {
4738                    return Err(GitError::InvalidFormat(
4739                        "upload-pack packfile response has frames after flush".into(),
4740                    ));
4741                }
4742            }
4743            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4744                return Err(GitError::InvalidFormat(
4745                    "upload-pack packfile response contains a non-flush control packet".into(),
4746                ));
4747            }
4748        }
4749    }
4750    if !saw_flush {
4751        return Err(GitError::InvalidFormat(
4752            "upload-pack packfile response missing flush".into(),
4753        ));
4754    }
4755    Ok(response)
4756}
4757
4758pub fn encode_upload_pack_packfile_response(
4759    response: &UploadPackPackfileResponse,
4760) -> Result<Vec<PktLineFrame>> {
4761    let mut frames = Vec::new();
4762    for acknowledgment in &response.acknowledgments {
4763        frames.push(PktLineFrame::data(encode_upload_pack_acknowledgment(
4764            acknowledgment,
4765        )?)?);
4766    }
4767    for packet in &response.sideband {
4768        frames.push(PktLineFrame::data(encode_sideband_packet(packet)?)?);
4769    }
4770    frames.push(PktLineFrame::Flush);
4771    Ok(frames)
4772}
4773
4774pub fn read_upload_pack_packfile_response(
4775    format: ObjectFormat,
4776    reader: &mut impl Read,
4777) -> Result<UploadPackPackfileResponse> {
4778    let frames = read_pkt_line_frames_until_flush(reader)?;
4779    parse_upload_pack_packfile_response(format, &frames)
4780}
4781
4782pub fn write_upload_pack_packfile_response(
4783    writer: &mut impl Write,
4784    response: &UploadPackPackfileResponse,
4785) -> Result<()> {
4786    for acknowledgment in &response.acknowledgments {
4787        write_upload_pack_acknowledgment(writer, acknowledgment)?;
4788    }
4789    for packet in &response.sideband {
4790        write_sideband_packet(writer, packet)?;
4791    }
4792    writer.write_all(b"0000")?;
4793    Ok(())
4794}
4795
4796pub fn demux_upload_pack_packfile_response(
4797    response: &UploadPackPackfileResponse,
4798) -> Result<SideBandDemux> {
4799    demux_sideband_packets(&response.sideband)
4800}
4801
4802pub fn parse_upload_pack_raw_packfile_response(
4803    format: ObjectFormat,
4804    input: &[u8],
4805) -> Result<UploadPackRawPackfileResponse> {
4806    let mut response = UploadPackRawPackfileResponse::default();
4807    let mut offset = 0usize;
4808    while offset < input.len() {
4809        match PktLineFrame::parse(&input[offset..]) {
4810            Ok((PktLineFrame::Data(payload), consumed)) => {
4811                let trimmed = trim_trailing_lf(&payload);
4812                if trimmed == b"NAK" || trimmed.starts_with(b"ACK ") {
4813                    response
4814                        .acknowledgments
4815                        .push(parse_upload_pack_acknowledgment(format, &payload)?);
4816                    offset += consumed;
4817                    continue;
4818                }
4819                return Err(GitError::InvalidFormat(
4820                    "upload-pack raw packfile response has non-ack pkt-line before packfile".into(),
4821                ));
4822            }
4823            Ok((PktLineFrame::Flush | PktLineFrame::Delimiter | PktLineFrame::ResponseEnd, _)) => {
4824                return Err(GitError::InvalidFormat(
4825                    "upload-pack raw packfile response contains a control packet".into(),
4826                ));
4827            }
4828            Err(_) if input[offset..].starts_with(b"PACK") => break,
4829            Err(err) => return Err(err),
4830        }
4831    }
4832    response.packfile = input[offset..].to_vec();
4833    if response.packfile.is_empty() {
4834        return Err(GitError::InvalidFormat(
4835            "upload-pack raw packfile response missing packfile".into(),
4836        ));
4837    }
4838    if !response.packfile.starts_with(b"PACK") {
4839        return Err(GitError::InvalidFormat(
4840            "upload-pack raw packfile response packfile must start with PACK".into(),
4841        ));
4842    }
4843    Ok(response)
4844}
4845
4846pub fn encode_upload_pack_raw_packfile_response(
4847    response: &UploadPackRawPackfileResponse,
4848) -> Result<Vec<u8>> {
4849    if response.packfile.is_empty() {
4850        return Err(GitError::InvalidFormat(
4851            "upload-pack raw packfile response missing packfile".into(),
4852        ));
4853    }
4854    if !response.packfile.starts_with(b"PACK") {
4855        return Err(GitError::InvalidFormat(
4856            "upload-pack raw packfile response packfile must start with PACK".into(),
4857        ));
4858    }
4859    let mut out = Vec::new();
4860    for acknowledgment in &response.acknowledgments {
4861        write_pkt_line_payload(
4862            &mut out,
4863            &encode_upload_pack_acknowledgment(acknowledgment)?,
4864        )?;
4865    }
4866    out.extend_from_slice(&response.packfile);
4867    Ok(out)
4868}
4869
4870pub fn read_upload_pack_raw_packfile_response(
4871    format: ObjectFormat,
4872    reader: &mut impl Read,
4873) -> Result<UploadPackRawPackfileResponse> {
4874    let mut input = Vec::new();
4875    reader.read_to_end(&mut input)?;
4876    parse_upload_pack_raw_packfile_response(format, &input)
4877}
4878
4879pub fn write_upload_pack_raw_packfile_response(
4880    writer: &mut impl Write,
4881    response: &UploadPackRawPackfileResponse,
4882) -> Result<()> {
4883    if response.packfile.is_empty() {
4884        return Err(GitError::InvalidFormat(
4885            "upload-pack raw packfile response missing packfile".into(),
4886        ));
4887    }
4888    if !response.packfile.starts_with(b"PACK") {
4889        return Err(GitError::InvalidFormat(
4890            "upload-pack raw packfile response packfile must start with PACK".into(),
4891        ));
4892    }
4893    for acknowledgment in &response.acknowledgments {
4894        write_upload_pack_acknowledgment(writer, acknowledgment)?;
4895    }
4896    writer.write_all(&response.packfile)?;
4897    Ok(())
4898}
4899
4900/// Parse the smart-HTTP/SSH v0 *shallow-info* section that precedes the packfile
4901/// when the upload-pack request carried `shallow`/`deepen`/`deepen-since`/
4902/// `deepen-not` arguments.
4903///
4904/// The section is zero or more `shallow <oid>` / `unshallow <oid>` pkt-lines
4905/// terminated by a flush-pkt; git always emits it (even empty, as a bare flush)
4906/// when the request was a deepen request, and never emits it otherwise. Returns
4907/// the parsed entries and the number of bytes consumed (through the flush) so the
4908/// caller can continue parsing the trailing acknowledgments + packfile from
4909/// `&input[consumed..]` (see [`parse_upload_pack_shallow_info_and_raw_packfile_response`]).
4910pub fn parse_upload_pack_shallow_info_section(
4911    format: ObjectFormat,
4912    input: &[u8],
4913) -> Result<(Vec<ProtocolV2FetchShallowInfo>, usize)> {
4914    let mut entries = Vec::new();
4915    let mut offset = 0usize;
4916    loop {
4917        let (frame, consumed) = PktLineFrame::parse(&input[offset..])?;
4918        offset += consumed;
4919        match frame {
4920            PktLineFrame::Data(payload) => {
4921                entries.push(parse_fetch_shallow_info(format, &payload)?)
4922            }
4923            PktLineFrame::Flush => return Ok((entries, offset)),
4924            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4925                return Err(GitError::InvalidFormat(
4926                    "upload-pack shallow-info section contains a non-flush control packet".into(),
4927                ));
4928            }
4929        }
4930    }
4931}
4932
4933/// Parse a raw upload-pack response that begins with a *shallow-info* section,
4934/// i.e. the response to a deepen request.
4935///
4936/// This is [`parse_upload_pack_raw_packfile_response`] preceded by the
4937/// shallow-info section ([`parse_upload_pack_shallow_info_section`]): it returns
4938/// the `shallow`/`unshallow` entries the server reported alongside the parsed
4939/// acknowledgments + raw packfile. Use it only when the request carried a
4940/// `shallow`/`deepen`/`deepen-since`/`deepen-not` argument; for a plain (non-deepen)
4941/// request the response has no leading shallow-info section and
4942/// [`parse_upload_pack_raw_packfile_response`] must be used instead.
4943pub fn parse_upload_pack_shallow_info_and_raw_packfile_response(
4944    format: ObjectFormat,
4945    input: &[u8],
4946) -> Result<(
4947    Vec<ProtocolV2FetchShallowInfo>,
4948    UploadPackRawPackfileResponse,
4949)> {
4950    let (shallow, consumed) = parse_upload_pack_shallow_info_section(format, input)?;
4951    let response = parse_upload_pack_raw_packfile_response(format, &input[consumed..])?;
4952    Ok((shallow, response))
4953}
4954
4955/// Read a raw upload-pack response that begins with a *shallow-info* section from
4956/// `reader`, returning the `shallow`/`unshallow` entries and the parsed
4957/// acknowledgments + raw packfile.
4958///
4959/// The reader counterpart of
4960/// [`parse_upload_pack_shallow_info_and_raw_packfile_response`]; see it for when
4961/// this applies.
4962pub fn read_upload_pack_shallow_info_and_raw_packfile_response(
4963    format: ObjectFormat,
4964    reader: &mut impl Read,
4965) -> Result<(
4966    Vec<ProtocolV2FetchShallowInfo>,
4967    UploadPackRawPackfileResponse,
4968)> {
4969    let mut input = Vec::new();
4970    reader.read_to_end(&mut input)?;
4971    parse_upload_pack_shallow_info_and_raw_packfile_response(format, &input)
4972}
4973
4974pub fn parse_receive_pack_request(
4975    format: ObjectFormat,
4976    frames: &[PktLineFrame],
4977) -> Result<ReceivePackRequest> {
4978    let mut request = ReceivePackRequest::default();
4979    let mut saw_command = false;
4980    let mut saw_flush = false;
4981    for (idx, frame) in frames.iter().enumerate() {
4982        match frame {
4983            PktLineFrame::Data(payload) if !saw_flush => {
4984                let payload = trim_trailing_lf(payload);
4985                if payload.is_empty() {
4986                    return Err(GitError::InvalidFormat(
4987                        "receive-pack request line is empty".into(),
4988                    ));
4989                }
4990                if let Some(shallow) = payload.strip_prefix(b"shallow ") {
4991                    if saw_command {
4992                        return Err(GitError::InvalidFormat(
4993                            "receive-pack request has shallow after commands".into(),
4994                        ));
4995                    }
4996                    let shallow = std::str::from_utf8(shallow)
4997                        .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
4998                    validate_protocol_v2_token("receive-pack shallow", shallow)?;
4999                    request.shallow.push(ObjectId::from_hex(format, shallow)?);
5000                    continue;
5001                }
5002
5003                let (command, capabilities) = match payload.iter().position(|byte| *byte == 0) {
5004                    Some(nul) if !saw_command => (
5005                        &payload[..nul],
5006                        Some(parse_capabilities(&payload[nul + 1..])?),
5007                    ),
5008                    Some(_) => {
5009                        return Err(GitError::InvalidFormat(
5010                            "receive-pack capabilities must appear on the first command".into(),
5011                        ));
5012                    }
5013                    None => (payload, None),
5014                };
5015                let command = parse_receive_pack_command(format, command)?;
5016                if let Some(capabilities) = capabilities {
5017                    request.capabilities = capabilities;
5018                }
5019                request.commands.push(command);
5020                saw_command = true;
5021            }
5022            PktLineFrame::Data(_) => {
5023                return Err(GitError::InvalidFormat(
5024                    "receive-pack request has data after flush".into(),
5025                ));
5026            }
5027            PktLineFrame::Flush => {
5028                saw_flush = true;
5029                if idx + 1 != frames.len() {
5030                    return Err(GitError::InvalidFormat(
5031                        "receive-pack request has frames after flush".into(),
5032                    ));
5033                }
5034            }
5035            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5036                return Err(GitError::InvalidFormat(
5037                    "receive-pack request contains a non-flush control packet".into(),
5038                ));
5039            }
5040        }
5041    }
5042    if !saw_flush {
5043        return Err(GitError::InvalidFormat(
5044            "receive-pack request missing flush".into(),
5045        ));
5046    }
5047    if !request.shallow.is_empty() && request.commands.is_empty() {
5048        return Err(GitError::InvalidFormat(
5049            "receive-pack request has shallow lines without commands".into(),
5050        ));
5051    }
5052    Ok(request)
5053}
5054
5055pub fn encode_receive_pack_request(request: &ReceivePackRequest) -> Result<Vec<PktLineFrame>> {
5056    if !request.shallow.is_empty() && request.commands.is_empty() {
5057        return Err(GitError::InvalidFormat(
5058            "receive-pack request has shallow lines without commands".into(),
5059        ));
5060    }
5061
5062    let mut frames = Vec::new();
5063    for oid in &request.shallow {
5064        frames.push(PktLineFrame::data(line_from_str(&format!(
5065            "shallow {oid}"
5066        )))?);
5067    }
5068    for (idx, command) in request.commands.iter().enumerate() {
5069        let mut payload = format_receive_pack_command(command)?;
5070        if idx == 0 && !request.capabilities.is_empty() {
5071            payload.push(0);
5072            payload.extend_from_slice(&encode_capabilities(&request.capabilities)?);
5073        }
5074        payload.push(b'\n');
5075        frames.push(PktLineFrame::data(payload)?);
5076    }
5077    frames.push(PktLineFrame::Flush);
5078    Ok(frames)
5079}
5080
5081pub fn read_receive_pack_request(
5082    format: ObjectFormat,
5083    reader: &mut impl Read,
5084) -> Result<ReceivePackRequest> {
5085    let frames = read_pkt_line_frames_until_flush(reader)?;
5086    parse_receive_pack_request(format, &frames)
5087}
5088
5089pub fn write_receive_pack_request(
5090    writer: &mut impl Write,
5091    request: &ReceivePackRequest,
5092) -> Result<()> {
5093    if !request.shallow.is_empty() && request.commands.is_empty() {
5094        return Err(GitError::InvalidFormat(
5095            "receive-pack request has shallow lines without commands".into(),
5096        ));
5097    }
5098
5099    for oid in &request.shallow {
5100        write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
5101    }
5102    for (idx, command) in request.commands.iter().enumerate() {
5103        let mut payload = format_receive_pack_command(command)?;
5104        if idx == 0 && !request.capabilities.is_empty() {
5105            payload.push(0);
5106            payload.extend_from_slice(&encode_capabilities(&request.capabilities)?);
5107        }
5108        payload.push(b'\n');
5109        write_pkt_line_payload(writer, &payload)?;
5110    }
5111    writer.write_all(b"0000")?;
5112    Ok(())
5113}
5114
5115pub fn parse_receive_pack_push_request(
5116    format: ObjectFormat,
5117    input: &[u8],
5118    has_push_options: bool,
5119) -> Result<ReceivePackPushRequest> {
5120    let (command_frames, consumed) = parse_pkt_line_frames_until_flush_from(input)?;
5121    let commands = parse_receive_pack_request(format, &command_frames)?;
5122    let mut offset = consumed;
5123    let push_options = if has_push_options {
5124        let (push_option_frames, consumed) =
5125            parse_pkt_line_frames_until_flush_from(&input[offset..])?;
5126        offset += consumed;
5127        Some(parse_receive_pack_push_options(&push_option_frames)?)
5128    } else {
5129        None
5130    };
5131    Ok(ReceivePackPushRequest {
5132        commands,
5133        push_options,
5134        packfile: input[offset..].to_vec(),
5135    })
5136}
5137
5138pub fn encode_receive_pack_push_request(request: &ReceivePackPushRequest) -> Result<Vec<u8>> {
5139    let mut out = Vec::new();
5140    write_receive_pack_request(&mut out, &request.commands)?;
5141    if let Some(push_options) = &request.push_options {
5142        write_receive_pack_push_options(&mut out, push_options)?;
5143    }
5144    out.extend_from_slice(&request.packfile);
5145    Ok(out)
5146}
5147
5148pub fn read_receive_pack_push_request(
5149    format: ObjectFormat,
5150    reader: &mut impl Read,
5151    has_push_options: bool,
5152) -> Result<ReceivePackPushRequest> {
5153    let commands = read_receive_pack_request(format, reader)?;
5154    let push_options = if has_push_options {
5155        Some(read_receive_pack_push_options(reader)?)
5156    } else {
5157        None
5158    };
5159    let mut packfile = Vec::new();
5160    reader.read_to_end(&mut packfile)?;
5161    Ok(ReceivePackPushRequest {
5162        commands,
5163        push_options,
5164        packfile,
5165    })
5166}
5167
5168pub fn write_receive_pack_push_request(
5169    writer: &mut impl Write,
5170    request: &ReceivePackPushRequest,
5171) -> Result<()> {
5172    write_receive_pack_request(writer, &request.commands)?;
5173    if let Some(push_options) = &request.push_options {
5174        write_receive_pack_push_options(writer, push_options)?;
5175    }
5176    writer.write_all(&request.packfile)?;
5177    Ok(())
5178}
5179
5180pub fn parse_receive_pack_features(capabilities: &[Capability]) -> Result<ReceivePackFeatures> {
5181    let mut features = ReceivePackFeatures::default();
5182    for capability in capabilities {
5183        match capability.name.as_str() {
5184            "report-status" => {
5185                reject_capability_value("receive-pack report-status", capability)?;
5186                if features.report_status {
5187                    return Err(GitError::InvalidFormat(
5188                        "receive-pack has duplicate report-status capability".into(),
5189                    ));
5190                }
5191                features.report_status = true;
5192            }
5193            "report-status-v2" => {
5194                reject_capability_value("receive-pack report-status-v2", capability)?;
5195                if features.report_status_v2 {
5196                    return Err(GitError::InvalidFormat(
5197                        "receive-pack has duplicate report-status-v2 capability".into(),
5198                    ));
5199                }
5200                features.report_status_v2 = true;
5201            }
5202            "delete-refs" => {
5203                reject_capability_value("receive-pack delete-refs", capability)?;
5204                if features.delete_refs {
5205                    return Err(GitError::InvalidFormat(
5206                        "receive-pack has duplicate delete-refs capability".into(),
5207                    ));
5208                }
5209                features.delete_refs = true;
5210            }
5211            "ofs-delta" => {
5212                reject_capability_value("receive-pack ofs-delta", capability)?;
5213                if features.ofs_delta {
5214                    return Err(GitError::InvalidFormat(
5215                        "receive-pack has duplicate ofs-delta capability".into(),
5216                    ));
5217                }
5218                features.ofs_delta = true;
5219            }
5220            "atomic" => {
5221                reject_capability_value("receive-pack atomic", capability)?;
5222                if features.atomic {
5223                    return Err(GitError::InvalidFormat(
5224                        "receive-pack has duplicate atomic capability".into(),
5225                    ));
5226                }
5227                features.atomic = true;
5228            }
5229            "push-options" => {
5230                reject_capability_value("receive-pack push-options", capability)?;
5231                if features.push_options {
5232                    return Err(GitError::InvalidFormat(
5233                        "receive-pack has duplicate push-options capability".into(),
5234                    ));
5235                }
5236                features.push_options = true;
5237            }
5238            "side-band-64k" => {
5239                reject_capability_value("receive-pack side-band-64k", capability)?;
5240                if features.side_band_64k {
5241                    return Err(GitError::InvalidFormat(
5242                        "receive-pack has duplicate side-band-64k capability".into(),
5243                    ));
5244                }
5245                features.side_band_64k = true;
5246            }
5247            "quiet" => {
5248                reject_capability_value("receive-pack quiet", capability)?;
5249                if features.quiet {
5250                    return Err(GitError::InvalidFormat(
5251                        "receive-pack has duplicate quiet capability".into(),
5252                    ));
5253                }
5254                features.quiet = true;
5255            }
5256            "no-thin" => {
5257                reject_capability_value("receive-pack no-thin", capability)?;
5258                if features.no_thin {
5259                    return Err(GitError::InvalidFormat(
5260                        "receive-pack has duplicate no-thin capability".into(),
5261                    ));
5262                }
5263                features.no_thin = true;
5264            }
5265            "agent" => {
5266                let Some(agent) = &capability.value else {
5267                    return Err(GitError::InvalidFormat(
5268                        "receive-pack agent capability is missing value".into(),
5269                    ));
5270                };
5271                if features.agent.is_some() {
5272                    return Err(GitError::InvalidFormat(
5273                        "receive-pack has duplicate agent capability".into(),
5274                    ));
5275                }
5276                validate_capability_field("receive-pack agent", agent)?;
5277                features.agent = Some(agent.clone());
5278            }
5279            "object-format" => {
5280                let Some(format) = &capability.value else {
5281                    return Err(GitError::InvalidFormat(
5282                        "receive-pack object-format capability is missing value".into(),
5283                    ));
5284                };
5285                if features.object_format.is_some() {
5286                    return Err(GitError::InvalidFormat(
5287                        "receive-pack has duplicate object-format capability".into(),
5288                    ));
5289                }
5290                validate_capability_field("receive-pack object-format", format)?;
5291                features.object_format = Some(format.parse()?);
5292            }
5293            _ => {
5294                encode_capabilities(std::slice::from_ref(capability))?;
5295                if features
5296                    .unknown
5297                    .iter()
5298                    .any(|known| known.name == capability.name)
5299                {
5300                    return Err(GitError::InvalidFormat(format!(
5301                        "receive-pack has duplicate {} capability",
5302                        capability.name
5303                    )));
5304                }
5305                features.unknown.push(capability.clone());
5306            }
5307        }
5308    }
5309    Ok(features)
5310}
5311
5312pub fn encode_receive_pack_features(features: &ReceivePackFeatures) -> Result<Vec<Capability>> {
5313    let mut capabilities = Vec::new();
5314    if features.report_status {
5315        capabilities.push(Capability {
5316            name: "report-status".into(),
5317            value: None,
5318        });
5319    }
5320    if features.report_status_v2 {
5321        capabilities.push(Capability {
5322            name: "report-status-v2".into(),
5323            value: None,
5324        });
5325    }
5326    if features.delete_refs {
5327        capabilities.push(Capability {
5328            name: "delete-refs".into(),
5329            value: None,
5330        });
5331    }
5332    if features.ofs_delta {
5333        capabilities.push(Capability {
5334            name: "ofs-delta".into(),
5335            value: None,
5336        });
5337    }
5338    if features.atomic {
5339        capabilities.push(Capability {
5340            name: "atomic".into(),
5341            value: None,
5342        });
5343    }
5344    if features.push_options {
5345        capabilities.push(Capability {
5346            name: "push-options".into(),
5347            value: None,
5348        });
5349    }
5350    if features.side_band_64k {
5351        capabilities.push(Capability {
5352            name: "side-band-64k".into(),
5353            value: None,
5354        });
5355    }
5356    if features.quiet {
5357        capabilities.push(Capability {
5358            name: "quiet".into(),
5359            value: None,
5360        });
5361    }
5362    if features.no_thin {
5363        capabilities.push(Capability {
5364            name: "no-thin".into(),
5365            value: None,
5366        });
5367    }
5368    if let Some(agent) = &features.agent {
5369        validate_capability_field("receive-pack agent", agent)?;
5370        capabilities.push(Capability {
5371            name: "agent".into(),
5372            value: Some(agent.clone()),
5373        });
5374    }
5375    if let Some(format) = features.object_format {
5376        capabilities.push(Capability {
5377            name: "object-format".into(),
5378            value: Some(format.name().into()),
5379        });
5380    }
5381    for capability in &features.unknown {
5382        if is_known_receive_pack_capability(&capability.name) {
5383            return Err(GitError::InvalidFormat(format!(
5384                "receive-pack unknown capability duplicates known capability {}",
5385                capability.name
5386            )));
5387        }
5388        encode_capabilities(std::slice::from_ref(capability))?;
5389        capabilities.push(capability.clone());
5390    }
5391    Ok(capabilities)
5392}
5393
5394pub fn validate_receive_pack_push_request_features(
5395    features: &ReceivePackFeatures,
5396    request: &ReceivePackPushRequest,
5397) -> Result<()> {
5398    for capability in &request.commands.capabilities {
5399        if matches!(
5400            capability.name.as_str(),
5401            "report-status"
5402                | "report-status-v2"
5403                | "delete-refs"
5404                | "ofs-delta"
5405                | "atomic"
5406                | "push-options"
5407                | "side-band-64k"
5408                | "quiet"
5409                | "no-thin"
5410        ) {
5411            reject_capability_value("receive-pack request capability", capability)?;
5412        }
5413        match capability.name.as_str() {
5414            "report-status" if !features.report_status => {
5415                return Err(GitError::InvalidFormat(
5416                    "receive-pack request uses report-status without advertised capability".into(),
5417                ));
5418            }
5419            "report-status-v2" if !features.report_status_v2 => {
5420                return Err(GitError::InvalidFormat(
5421                    "receive-pack request uses report-status-v2 without advertised capability"
5422                        .into(),
5423                ));
5424            }
5425            "delete-refs" if !features.delete_refs => {
5426                return Err(GitError::InvalidFormat(
5427                    "receive-pack request uses delete-refs without advertised capability".into(),
5428                ));
5429            }
5430            "ofs-delta" if !features.ofs_delta => {
5431                return Err(GitError::InvalidFormat(
5432                    "receive-pack request uses ofs-delta without advertised capability".into(),
5433                ));
5434            }
5435            "atomic" if !features.atomic => {
5436                return Err(GitError::InvalidFormat(
5437                    "receive-pack request uses atomic without advertised capability".into(),
5438                ));
5439            }
5440            "push-options" if !features.push_options => {
5441                return Err(GitError::InvalidFormat(
5442                    "receive-pack request uses push-options without advertised capability".into(),
5443                ));
5444            }
5445            "side-band-64k" if !features.side_band_64k => {
5446                return Err(GitError::InvalidFormat(
5447                    "receive-pack request uses side-band-64k without advertised capability".into(),
5448                ));
5449            }
5450            "quiet" if !features.quiet => {
5451                return Err(GitError::InvalidFormat(
5452                    "receive-pack request uses quiet without advertised capability".into(),
5453                ));
5454            }
5455            "no-thin" => {
5456                return Err(GitError::InvalidFormat(
5457                    "receive-pack request must not request no-thin".into(),
5458                ));
5459            }
5460            "agent" => {
5461                validate_capability_field(
5462                    "receive-pack request agent",
5463                    capability.value.as_deref().unwrap_or_default(),
5464                )?;
5465            }
5466            "object-format" => {
5467                let Some(value) = &capability.value else {
5468                    return Err(GitError::InvalidFormat(
5469                        "receive-pack request object-format capability is missing value".into(),
5470                    ));
5471                };
5472                let requested_format: ObjectFormat = value.parse()?;
5473                if features.object_format != Some(requested_format) {
5474                    return Err(GitError::InvalidFormat(
5475                        "receive-pack request object-format was not advertised".into(),
5476                    ));
5477                }
5478            }
5479            name if is_known_receive_pack_capability(name) => {}
5480            _ => {
5481                if !features
5482                    .unknown
5483                    .iter()
5484                    .any(|advertised| advertised.name == capability.name)
5485                {
5486                    return Err(GitError::InvalidFormat(format!(
5487                        "receive-pack request uses unadvertised capability {}",
5488                        capability.name
5489                    )));
5490                }
5491            }
5492        }
5493    }
5494
5495    let requested_push_options = request
5496        .commands
5497        .capabilities
5498        .iter()
5499        .any(|capability| capability.name == "push-options");
5500    match (requested_push_options, &request.push_options) {
5501        (true, Some(_)) => {}
5502        (true, None) => {
5503            return Err(GitError::InvalidFormat(
5504                "receive-pack request uses push-options without push-options section".into(),
5505            ));
5506        }
5507        (false, Some(_)) => {
5508            return Err(GitError::InvalidFormat(
5509                "receive-pack request has push-options section without requested capability".into(),
5510            ));
5511        }
5512        (false, None) => {}
5513    }
5514
5515    let has_delete = request
5516        .commands
5517        .commands
5518        .iter()
5519        .any(is_receive_pack_delete_command);
5520    if has_delete && !features.delete_refs {
5521        return Err(GitError::InvalidFormat(
5522            "receive-pack request deletes refs without advertised delete-refs capability".into(),
5523        ));
5524    }
5525
5526    let has_update_or_create = request
5527        .commands
5528        .commands
5529        .iter()
5530        .any(|command| !is_receive_pack_delete_command(command));
5531    if has_update_or_create && request.packfile.is_empty() {
5532        return Err(GitError::InvalidFormat(
5533            "receive-pack request with create/update commands is missing packfile".into(),
5534        ));
5535    }
5536    if !has_update_or_create && !request.packfile.is_empty() {
5537        return Err(GitError::InvalidFormat(
5538            "receive-pack delete-only request must not include packfile".into(),
5539        ));
5540    }
5541    Ok(())
5542}
5543
5544pub fn apply_receive_pack_push_request<R, I, C, U, D>(
5545    features: &ReceivePackFeatures,
5546    request: &ReceivePackPushRequest,
5547    mut read_ref: R,
5548    mut install_pack: I,
5549    mut contains_object: C,
5550    mut apply_updates: U,
5551    mut delete_ref: D,
5552) -> Result<ReceivePackReportStatus>
5553where
5554    R: FnMut(&str) -> Result<Option<ObjectId>>,
5555    I: FnMut(&[u8]) -> Result<()>,
5556    C: FnMut(&ObjectId) -> Result<bool>,
5557    U: FnMut(&[ReceivePackCommand]) -> Result<()>,
5558    D: FnMut(&ReceivePackCommand) -> Result<()>,
5559{
5560    validate_receive_pack_push_request_features(features, request)?;
5561
5562    for command in request
5563        .commands
5564        .commands
5565        .iter()
5566        .filter(|command| is_receive_pack_delete_command(command))
5567    {
5568        if !command.old_id.is_null() && read_ref(&command.name)? != Some(command.old_id.clone()) {
5569            return Err(GitError::Transaction(format!(
5570                "expected ref {} to match",
5571                command.name
5572            )));
5573        }
5574    }
5575
5576    let updates = request
5577        .commands
5578        .commands
5579        .iter()
5580        .filter(|command| !is_receive_pack_delete_command(command))
5581        .cloned()
5582        .collect::<Vec<_>>();
5583    if !updates.is_empty() {
5584        install_pack(&request.packfile)?;
5585        for command in &updates {
5586            if !contains_object(&command.new_id)? {
5587                return Err(GitError::InvalidObject(format!(
5588                    "receive-pack packfile did not provide {}",
5589                    command.new_id
5590                )));
5591            }
5592        }
5593        apply_updates(&updates)?;
5594    }
5595
5596    for command in request
5597        .commands
5598        .commands
5599        .iter()
5600        .filter(|command| is_receive_pack_delete_command(command))
5601    {
5602        delete_ref(command)?;
5603    }
5604
5605    Ok(ReceivePackReportStatus {
5606        unpack: ReceivePackUnpackStatus::Ok,
5607        commands: request
5608            .commands
5609            .commands
5610            .iter()
5611            .map(|command| ReceivePackCommandStatus::Ok {
5612                name: command.name.clone(),
5613            })
5614            .collect(),
5615    })
5616}
5617
5618pub fn parse_receive_pack_report_status(
5619    frames: &[PktLineFrame],
5620) -> Result<ReceivePackReportStatus> {
5621    let Some((first, rest)) = frames.split_first() else {
5622        return Err(GitError::InvalidFormat(
5623            "receive-pack report-status is empty".into(),
5624        ));
5625    };
5626    let PktLineFrame::Data(payload) = first else {
5627        return Err(GitError::InvalidFormat(
5628            "receive-pack report-status must start with unpack status".into(),
5629        ));
5630    };
5631    let unpack = parse_receive_pack_unpack_status(payload)?;
5632
5633    let mut commands = Vec::new();
5634    let mut saw_flush = false;
5635    for (idx, frame) in rest.iter().enumerate() {
5636        match frame {
5637            PktLineFrame::Data(payload) if !saw_flush => {
5638                commands.push(parse_receive_pack_command_status(payload)?);
5639            }
5640            PktLineFrame::Data(_) => {
5641                return Err(GitError::InvalidFormat(
5642                    "receive-pack report-status has data after flush".into(),
5643                ));
5644            }
5645            PktLineFrame::Flush => {
5646                saw_flush = true;
5647                if idx + 1 != rest.len() {
5648                    return Err(GitError::InvalidFormat(
5649                        "receive-pack report-status has frames after flush".into(),
5650                    ));
5651                }
5652            }
5653            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5654                return Err(GitError::InvalidFormat(
5655                    "receive-pack report-status contains a non-flush control packet".into(),
5656                ));
5657            }
5658        }
5659    }
5660    if !saw_flush {
5661        return Err(GitError::InvalidFormat(
5662            "receive-pack report-status missing flush".into(),
5663        ));
5664    }
5665    Ok(ReceivePackReportStatus { unpack, commands })
5666}
5667
5668pub fn encode_receive_pack_report_status(
5669    report: &ReceivePackReportStatus,
5670) -> Result<Vec<PktLineFrame>> {
5671    let mut frames = Vec::new();
5672    frames.push(PktLineFrame::data(line_from_str(
5673        &format_receive_pack_unpack_status(&report.unpack)?,
5674    ))?);
5675    for command in &report.commands {
5676        frames.push(PktLineFrame::data(line_from_str(
5677            &format_receive_pack_command_status(command)?,
5678        ))?);
5679    }
5680    frames.push(PktLineFrame::Flush);
5681    Ok(frames)
5682}
5683
5684pub fn read_receive_pack_report_status(reader: &mut impl Read) -> Result<ReceivePackReportStatus> {
5685    let frames = read_pkt_line_frames_until_flush(reader)?;
5686    parse_receive_pack_report_status(&frames)
5687}
5688
5689pub fn write_receive_pack_report_status(
5690    writer: &mut impl Write,
5691    report: &ReceivePackReportStatus,
5692) -> Result<()> {
5693    write_pkt_line_payload(
5694        writer,
5695        &line_from_str(&format_receive_pack_unpack_status(&report.unpack)?),
5696    )?;
5697    for command in &report.commands {
5698        write_pkt_line_payload(
5699            writer,
5700            &line_from_str(&format_receive_pack_command_status(command)?),
5701        )?;
5702    }
5703    writer.write_all(b"0000")?;
5704    Ok(())
5705}
5706
5707pub fn parse_receive_pack_report_status_v2(
5708    format: ObjectFormat,
5709    frames: &[PktLineFrame],
5710) -> Result<ReceivePackReportStatusV2> {
5711    let Some((first, rest)) = frames.split_first() else {
5712        return Err(GitError::InvalidFormat(
5713            "receive-pack report-status-v2 is empty".into(),
5714        ));
5715    };
5716    let PktLineFrame::Data(payload) = first else {
5717        return Err(GitError::InvalidFormat(
5718            "receive-pack report-status-v2 must start with unpack status".into(),
5719        ));
5720    };
5721    let unpack = parse_receive_pack_unpack_status(payload)?;
5722
5723    let mut commands = Vec::new();
5724    let mut saw_flush = false;
5725    for (idx, frame) in rest.iter().enumerate() {
5726        match frame {
5727            PktLineFrame::Data(payload) if !saw_flush => {
5728                let text =
5729                    parse_protocol_v2_line_text("receive-pack report-status-v2 line", payload)?;
5730                if text.starts_with("option ") {
5731                    let Some(ReceivePackCommandStatusV2::Ok { options, .. }) = commands.last_mut()
5732                    else {
5733                        return Err(GitError::InvalidFormat(
5734                            "receive-pack report-status-v2 option without ok status".into(),
5735                        ));
5736                    };
5737                    parse_receive_pack_report_status_v2_option(format, text, options)?;
5738                } else {
5739                    commands.push(parse_receive_pack_command_status_v2(text)?);
5740                }
5741            }
5742            PktLineFrame::Data(_) => {
5743                return Err(GitError::InvalidFormat(
5744                    "receive-pack report-status-v2 has data after flush".into(),
5745                ));
5746            }
5747            PktLineFrame::Flush => {
5748                saw_flush = true;
5749                if idx + 1 != rest.len() {
5750                    return Err(GitError::InvalidFormat(
5751                        "receive-pack report-status-v2 has frames after flush".into(),
5752                    ));
5753                }
5754            }
5755            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5756                return Err(GitError::InvalidFormat(
5757                    "receive-pack report-status-v2 contains a non-flush control packet".into(),
5758                ));
5759            }
5760        }
5761    }
5762    if !saw_flush {
5763        return Err(GitError::InvalidFormat(
5764            "receive-pack report-status-v2 missing flush".into(),
5765        ));
5766    }
5767    Ok(ReceivePackReportStatusV2 { unpack, commands })
5768}
5769
5770pub fn encode_receive_pack_report_status_v2(
5771    report: &ReceivePackReportStatusV2,
5772) -> Result<Vec<PktLineFrame>> {
5773    let mut frames = Vec::new();
5774    frames.push(PktLineFrame::data(line_from_str(
5775        &format_receive_pack_unpack_status(&report.unpack)?,
5776    ))?);
5777    for command in &report.commands {
5778        frames.push(PktLineFrame::data(line_from_str(
5779            &format_receive_pack_command_status_v2(command)?,
5780        ))?);
5781        if let ReceivePackCommandStatusV2::Ok { options, .. } = command {
5782            for option in format_receive_pack_report_status_v2_options(options)? {
5783                frames.push(PktLineFrame::data(line_from_str(&option))?);
5784            }
5785        }
5786    }
5787    frames.push(PktLineFrame::Flush);
5788    Ok(frames)
5789}
5790
5791pub fn read_receive_pack_report_status_v2(
5792    format: ObjectFormat,
5793    reader: &mut impl Read,
5794) -> Result<ReceivePackReportStatusV2> {
5795    let frames = read_pkt_line_frames_until_flush(reader)?;
5796    parse_receive_pack_report_status_v2(format, &frames)
5797}
5798
5799pub fn write_receive_pack_report_status_v2(
5800    writer: &mut impl Write,
5801    report: &ReceivePackReportStatusV2,
5802) -> Result<()> {
5803    write_pkt_line_payload(
5804        writer,
5805        &line_from_str(&format_receive_pack_unpack_status(&report.unpack)?),
5806    )?;
5807    for command in &report.commands {
5808        write_pkt_line_payload(
5809            writer,
5810            &line_from_str(&format_receive_pack_command_status_v2(command)?),
5811        )?;
5812        if let ReceivePackCommandStatusV2::Ok { options, .. } = command {
5813            for option in format_receive_pack_report_status_v2_options(options)? {
5814                write_pkt_line_payload(writer, &line_from_str(&option))?;
5815            }
5816        }
5817    }
5818    writer.write_all(b"0000")?;
5819    Ok(())
5820}
5821
5822pub fn parse_receive_pack_push_options(frames: &[PktLineFrame]) -> Result<Vec<String>> {
5823    let mut options = Vec::new();
5824    let mut saw_flush = false;
5825    for (idx, frame) in frames.iter().enumerate() {
5826        match frame {
5827            PktLineFrame::Data(payload) if !saw_flush => {
5828                let option = trim_trailing_lf(payload);
5829                validate_receive_pack_push_option(option)?;
5830                options.push(
5831                    std::str::from_utf8(option)
5832                        .map_err(|err| GitError::InvalidFormat(err.to_string()))?
5833                        .to_string(),
5834                );
5835            }
5836            PktLineFrame::Data(_) => {
5837                return Err(GitError::InvalidFormat(
5838                    "receive-pack push-options has data after flush".into(),
5839                ));
5840            }
5841            PktLineFrame::Flush => {
5842                saw_flush = true;
5843                if idx + 1 != frames.len() {
5844                    return Err(GitError::InvalidFormat(
5845                        "receive-pack push-options has frames after flush".into(),
5846                    ));
5847                }
5848            }
5849            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5850                return Err(GitError::InvalidFormat(
5851                    "receive-pack push-options contains a non-flush control packet".into(),
5852                ));
5853            }
5854        }
5855    }
5856    if !saw_flush {
5857        return Err(GitError::InvalidFormat(
5858            "receive-pack push-options missing flush".into(),
5859        ));
5860    }
5861    Ok(options)
5862}
5863
5864pub fn encode_receive_pack_push_options(options: &[String]) -> Result<Vec<PktLineFrame>> {
5865    let mut frames = Vec::new();
5866    for option in options {
5867        validate_receive_pack_push_option(option.as_bytes())?;
5868        let mut payload = option.as_bytes().to_vec();
5869        payload.push(b'\n');
5870        frames.push(PktLineFrame::data(payload)?);
5871    }
5872    frames.push(PktLineFrame::Flush);
5873    Ok(frames)
5874}
5875
5876pub fn read_receive_pack_push_options(reader: &mut impl Read) -> Result<Vec<String>> {
5877    let frames = read_pkt_line_frames_until_flush(reader)?;
5878    parse_receive_pack_push_options(&frames)
5879}
5880
5881pub fn write_receive_pack_push_options(writer: &mut impl Write, options: &[String]) -> Result<()> {
5882    for option in options {
5883        validate_receive_pack_push_option(option.as_bytes())?;
5884        let mut payload = option.as_bytes().to_vec();
5885        payload.push(b'\n');
5886        write_pkt_line_payload(writer, &payload)?;
5887    }
5888    writer.write_all(b"0000")?;
5889    Ok(())
5890}
5891
5892fn parse_receive_pack_command(format: ObjectFormat, payload: &[u8]) -> Result<ReceivePackCommand> {
5893    let text =
5894        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
5895    let mut fields = text.split(' ');
5896    let old_id = fields
5897        .next()
5898        .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing old id".into()))?;
5899    let new_id = fields
5900        .next()
5901        .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing new id".into()))?;
5902    let name = fields
5903        .next()
5904        .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing ref name".into()))?;
5905    if fields.next().is_some() {
5906        return Err(GitError::InvalidFormat(
5907            "receive-pack command has too many fields".into(),
5908        ));
5909    }
5910    validate_protocol_v2_token("receive-pack old id", old_id)?;
5911    validate_protocol_v2_token("receive-pack new id", new_id)?;
5912    validate_protocol_v2_token("receive-pack ref name", name)?;
5913    Ok(ReceivePackCommand {
5914        old_id: ObjectId::from_hex(format, old_id)?,
5915        new_id: ObjectId::from_hex(format, new_id)?,
5916        name: name.to_string(),
5917    })
5918}
5919
5920fn format_receive_pack_command(command: &ReceivePackCommand) -> Result<Vec<u8>> {
5921    if command.old_id.format() != command.new_id.format() {
5922        return Err(GitError::InvalidObjectId(
5923            "receive-pack command object formats do not match".into(),
5924        ));
5925    }
5926    validate_protocol_v2_token("receive-pack ref name", &command.name)?;
5927    Ok(format!("{} {} {}", command.old_id, command.new_id, command.name).into_bytes())
5928}
5929
5930fn set_upload_pack_flag(value: &mut bool, capability: &Capability) -> Result<()> {
5931    reject_capability_value("upload-pack capability", capability)?;
5932    if *value {
5933        return Err(GitError::InvalidFormat(format!(
5934            "upload-pack has duplicate {} capability",
5935            capability.name
5936        )));
5937    }
5938    *value = true;
5939    Ok(())
5940}
5941
5942fn push_upload_pack_flag(capabilities: &mut Vec<Capability>, name: &str, enabled: bool) {
5943    if enabled {
5944        capabilities.push(Capability {
5945            name: name.into(),
5946            value: None,
5947        });
5948    }
5949}
5950
5951fn is_known_upload_pack_capability(name: &str) -> bool {
5952    matches!(
5953        name,
5954        "multi_ack"
5955            | "multi_ack_detailed"
5956            | "no-done"
5957            | "thin-pack"
5958            | "side-band"
5959            | "side-band-64k"
5960            | "ofs-delta"
5961            | "shallow"
5962            | "deepen-since"
5963            | "deepen-not"
5964            | "include-tag"
5965            | "no-progress"
5966            | "allow-tip-sha1-in-want"
5967            | "allow-reachable-sha1-in-want"
5968            | "filter"
5969            | "agent"
5970            | "object-format"
5971            | "symref"
5972    )
5973}
5974
5975fn is_upload_pack_flag_capability(name: &str) -> bool {
5976    matches!(
5977        name,
5978        "multi_ack"
5979            | "multi_ack_detailed"
5980            | "no-done"
5981            | "thin-pack"
5982            | "side-band"
5983            | "side-band-64k"
5984            | "ofs-delta"
5985            | "shallow"
5986            | "deepen-since"
5987            | "deepen-not"
5988            | "include-tag"
5989            | "no-progress"
5990            | "allow-tip-sha1-in-want"
5991            | "allow-reachable-sha1-in-want"
5992            | "filter"
5993    )
5994}
5995
5996fn reject_capability_value(label: &str, capability: &Capability) -> Result<()> {
5997    if capability.value.is_some() {
5998        return Err(GitError::InvalidFormat(format!(
5999            "{label} must not have value"
6000        )));
6001    }
6002    Ok(())
6003}
6004
6005fn is_known_receive_pack_capability(name: &str) -> bool {
6006    matches!(
6007        name,
6008        "report-status"
6009            | "report-status-v2"
6010            | "delete-refs"
6011            | "ofs-delta"
6012            | "atomic"
6013            | "push-options"
6014            | "side-band-64k"
6015            | "quiet"
6016            | "no-thin"
6017            | "agent"
6018            | "object-format"
6019    )
6020}
6021
6022fn is_receive_pack_delete_command(command: &ReceivePackCommand) -> bool {
6023    command.new_id.is_null()
6024}
6025
6026fn parse_receive_pack_unpack_status(payload: &[u8]) -> Result<ReceivePackUnpackStatus> {
6027    let text = parse_protocol_v2_line_text("receive-pack unpack status", payload)?;
6028    if text == "unpack ok" {
6029        return Ok(ReceivePackUnpackStatus::Ok);
6030    }
6031    let Some(message) = text.strip_prefix("unpack ") else {
6032        return Err(GitError::InvalidFormat(format!(
6033            "unsupported receive-pack unpack status {text}"
6034        )));
6035    };
6036    validate_receive_pack_status_message("receive-pack unpack error", message)?;
6037    Ok(ReceivePackUnpackStatus::Error(message.to_string()))
6038}
6039
6040fn format_receive_pack_unpack_status(status: &ReceivePackUnpackStatus) -> Result<String> {
6041    match status {
6042        ReceivePackUnpackStatus::Ok => Ok("unpack ok".into()),
6043        ReceivePackUnpackStatus::Error(message) => {
6044            validate_receive_pack_status_message("receive-pack unpack error", message)?;
6045            Ok(format!("unpack {message}"))
6046        }
6047    }
6048}
6049
6050fn parse_receive_pack_command_status(payload: &[u8]) -> Result<ReceivePackCommandStatus> {
6051    let text = parse_protocol_v2_line_text("receive-pack command status", payload)?;
6052    if let Some(name) = text.strip_prefix("ok ") {
6053        validate_protocol_v2_token("receive-pack status ref name", name)?;
6054        return Ok(ReceivePackCommandStatus::Ok {
6055            name: name.to_string(),
6056        });
6057    }
6058    let Some(rest) = text.strip_prefix("ng ") else {
6059        return Err(GitError::InvalidFormat(format!(
6060            "unsupported receive-pack command status {text}"
6061        )));
6062    };
6063    let (name, message) = rest
6064        .split_once(' ')
6065        .ok_or_else(|| GitError::InvalidFormat("receive-pack ng status missing message".into()))?;
6066    validate_protocol_v2_token("receive-pack status ref name", name)?;
6067    validate_receive_pack_status_message("receive-pack ng status message", message)?;
6068    Ok(ReceivePackCommandStatus::Ng {
6069        name: name.to_string(),
6070        message: message.to_string(),
6071    })
6072}
6073
6074fn format_receive_pack_command_status(status: &ReceivePackCommandStatus) -> Result<String> {
6075    match status {
6076        ReceivePackCommandStatus::Ok { name } => {
6077            validate_protocol_v2_token("receive-pack status ref name", name)?;
6078            Ok(format!("ok {name}"))
6079        }
6080        ReceivePackCommandStatus::Ng { name, message } => {
6081            validate_protocol_v2_token("receive-pack status ref name", name)?;
6082            validate_receive_pack_status_message("receive-pack ng status message", message)?;
6083            Ok(format!("ng {name} {message}"))
6084        }
6085    }
6086}
6087
6088fn parse_receive_pack_command_status_v2(text: &str) -> Result<ReceivePackCommandStatusV2> {
6089    if let Some(name) = text.strip_prefix("ok ") {
6090        validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6091        return Ok(ReceivePackCommandStatusV2::Ok {
6092            name: name.to_string(),
6093            options: ReceivePackCommandStatusV2Options::default(),
6094        });
6095    }
6096    let Some(rest) = text.strip_prefix("ng ") else {
6097        return Err(GitError::InvalidFormat(format!(
6098            "unsupported receive-pack report-status-v2 line {text}"
6099        )));
6100    };
6101    let (name, message) = rest.split_once(' ').ok_or_else(|| {
6102        GitError::InvalidFormat("receive-pack status-v2 ng status missing message".into())
6103    })?;
6104    validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6105    validate_receive_pack_status_message("receive-pack status-v2 ng message", message)?;
6106    Ok(ReceivePackCommandStatusV2::Ng {
6107        name: name.to_string(),
6108        message: message.to_string(),
6109    })
6110}
6111
6112fn format_receive_pack_command_status_v2(status: &ReceivePackCommandStatusV2) -> Result<String> {
6113    match status {
6114        ReceivePackCommandStatusV2::Ok { name, .. } => {
6115            validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6116            Ok(format!("ok {name}"))
6117        }
6118        ReceivePackCommandStatusV2::Ng { name, message } => {
6119            validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6120            validate_receive_pack_status_message("receive-pack status-v2 ng message", message)?;
6121            Ok(format!("ng {name} {message}"))
6122        }
6123    }
6124}
6125
6126fn parse_receive_pack_report_status_v2_option(
6127    format: ObjectFormat,
6128    text: &str,
6129    options: &mut ReceivePackCommandStatusV2Options,
6130) -> Result<()> {
6131    if let Some(refname) = text.strip_prefix("option refname ") {
6132        if options.refname.is_some() {
6133            return Err(GitError::InvalidFormat(
6134                "receive-pack report-status-v2 has duplicate refname option".into(),
6135            ));
6136        }
6137        validate_protocol_v2_token("receive-pack status-v2 option refname", refname)?;
6138        options.refname = Some(refname.to_string());
6139    } else if let Some(old_oid) = text.strip_prefix("option old-oid ") {
6140        if options.old_oid.is_some() {
6141            return Err(GitError::InvalidFormat(
6142                "receive-pack report-status-v2 has duplicate old-oid option".into(),
6143            ));
6144        }
6145        validate_protocol_v2_token("receive-pack status-v2 option old-oid", old_oid)?;
6146        options.old_oid = Some(ObjectId::from_hex(format, old_oid)?);
6147    } else if let Some(new_oid) = text.strip_prefix("option new-oid ") {
6148        if options.new_oid.is_some() {
6149            return Err(GitError::InvalidFormat(
6150                "receive-pack report-status-v2 has duplicate new-oid option".into(),
6151            ));
6152        }
6153        validate_protocol_v2_token("receive-pack status-v2 option new-oid", new_oid)?;
6154        options.new_oid = Some(ObjectId::from_hex(format, new_oid)?);
6155    } else if text == "option forced-update" {
6156        if options.forced_update {
6157            return Err(GitError::InvalidFormat(
6158                "receive-pack report-status-v2 has duplicate forced-update option".into(),
6159            ));
6160        }
6161        options.forced_update = true;
6162    } else {
6163        return Err(GitError::InvalidFormat(format!(
6164            "unsupported receive-pack report-status-v2 option {text}"
6165        )));
6166    }
6167    Ok(())
6168}
6169
6170fn format_receive_pack_report_status_v2_options(
6171    options: &ReceivePackCommandStatusV2Options,
6172) -> Result<Vec<String>> {
6173    let mut out = Vec::new();
6174    if let Some(refname) = &options.refname {
6175        validate_protocol_v2_token("receive-pack status-v2 option refname", refname)?;
6176        out.push(format!("option refname {refname}"));
6177    }
6178    if let Some(old_oid) = &options.old_oid {
6179        out.push(format!("option old-oid {old_oid}"));
6180    }
6181    if let Some(new_oid) = &options.new_oid {
6182        out.push(format!("option new-oid {new_oid}"));
6183    }
6184    if options.forced_update {
6185        out.push("option forced-update".into());
6186    }
6187    Ok(out)
6188}
6189
6190fn validate_receive_pack_status_message(label: &str, message: &str) -> Result<()> {
6191    if message.is_empty() {
6192        return Err(GitError::InvalidFormat(format!("{label} is empty")));
6193    }
6194    if message
6195        .bytes()
6196        .any(|byte| matches!(byte, b'\n' | b'\r' | 0))
6197    {
6198        return Err(GitError::InvalidFormat(format!(
6199            "{label} contains a delimiter byte"
6200        )));
6201    }
6202    Ok(())
6203}
6204
6205fn validate_receive_pack_push_option(option: &[u8]) -> Result<()> {
6206    if option.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
6207        return Err(GitError::InvalidFormat(
6208            "receive-pack push-option contains a delimiter byte".into(),
6209        ));
6210    }
6211    Ok(())
6212}
6213
6214fn validate_protocol_error_message(message: &str) -> Result<()> {
6215    if message.is_empty() {
6216        return Err(GitError::InvalidFormat(
6217            "protocol error message is empty".into(),
6218        ));
6219    }
6220    if message
6221        .bytes()
6222        .any(|byte| matches!(byte, b'\n' | b'\r' | 0))
6223    {
6224        return Err(GitError::InvalidFormat(
6225            "protocol error message contains a delimiter byte".into(),
6226        ));
6227    }
6228    Ok(())
6229}
6230
6231fn parse_capability_token(token: &str) -> Result<Capability> {
6232    if token.is_empty() {
6233        return Err(GitError::InvalidFormat("empty capability token".into()));
6234    }
6235    let (name, value) = token
6236        .split_once('=')
6237        .map_or((token, None), |(name, value)| (name, Some(value)));
6238    validate_capability_field("capability name", name)?;
6239    if let Some(value) = value {
6240        validate_capability_field("capability value", value)?;
6241    }
6242    Ok(Capability {
6243        name: name.to_string(),
6244        value: value.map(str::to_string),
6245    })
6246}
6247
6248fn parse_protocol_v2_capability_line(payload: &[u8]) -> Result<Capability> {
6249    let payload = trim_trailing_lf(payload);
6250    if payload.is_empty() {
6251        return Err(GitError::InvalidFormat(
6252            "empty protocol v2 capability line".into(),
6253        ));
6254    }
6255    let text =
6256        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6257    let (name, value) = text
6258        .split_once('=')
6259        .map_or((text, None), |(name, value)| (name, Some(value)));
6260    validate_capability_name(name)?;
6261    if let Some(value) = value {
6262        validate_protocol_v2_capability_value(value)?;
6263    }
6264    Ok(Capability {
6265        name: name.to_string(),
6266        value: value.map(str::to_string),
6267    })
6268}
6269
6270fn parse_protocol_v2_command_line(payload: &[u8]) -> Result<String> {
6271    let payload = trim_trailing_lf(payload);
6272    let text =
6273        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6274    let Some(command) = text.strip_prefix("command=") else {
6275        return Err(GitError::InvalidFormat(
6276            "protocol v2 command request missing command prefix".into(),
6277        ));
6278    };
6279    validate_capability_name(command)?;
6280    Ok(command.to_string())
6281}
6282
6283fn parse_protocol_v2_ls_refs_line(
6284    format: ObjectFormat,
6285    payload: &[u8],
6286) -> Result<ProtocolV2LsRefsRecord> {
6287    let payload = trim_trailing_lf(payload);
6288    if payload.is_empty() {
6289        return Err(GitError::InvalidFormat(
6290            "ls-refs response line is empty".into(),
6291        ));
6292    }
6293    let text =
6294        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6295    let mut fields = text.split(' ');
6296    let first = fields
6297        .next()
6298        .ok_or_else(|| GitError::InvalidFormat("ls-refs response line is empty".into()))?;
6299    if first == "unborn" {
6300        let name = fields
6301            .next()
6302            .ok_or_else(|| GitError::InvalidFormat("ls-refs unborn line is missing name".into()))?;
6303        validate_protocol_v2_token("ls-refs ref name", name)?;
6304        let (symref_target, attributes) = parse_protocol_v2_ls_refs_attributes(format, fields)?;
6305        return Ok(ProtocolV2LsRefsRecord::Unborn {
6306            name: name.to_string(),
6307            symref_target,
6308            attributes,
6309        });
6310    }
6311
6312    let oid = ObjectId::from_hex(format, first)?;
6313    let name = fields
6314        .next()
6315        .ok_or_else(|| GitError::InvalidFormat("ls-refs ref line is missing name".into()))?;
6316    validate_protocol_v2_token("ls-refs ref name", name)?;
6317    let (peeled, symref_target, attributes) =
6318        parse_protocol_v2_ls_refs_ref_attributes(format, fields)?;
6319    Ok(ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
6320        oid,
6321        name: name.to_string(),
6322        peeled,
6323        symref_target,
6324        attributes,
6325    }))
6326}
6327
6328fn parse_protocol_v2_ls_refs_ref_attributes<'a>(
6329    format: ObjectFormat,
6330    fields: impl Iterator<Item = &'a str>,
6331) -> Result<(Option<ObjectId>, Option<String>, Vec<String>)> {
6332    let mut peeled = None;
6333    let (symref_target, attributes) =
6334        parse_protocol_v2_ls_refs_attributes_with(format, fields, |attr| {
6335            if let Some(value) = attr.strip_prefix("peeled:") {
6336                if peeled.is_some() {
6337                    return Err(GitError::InvalidFormat(
6338                        "ls-refs response has duplicate peeled attribute".into(),
6339                    ));
6340                }
6341                peeled = Some(ObjectId::from_hex(format, value)?);
6342                return Ok(true);
6343            }
6344            Ok(false)
6345        })?;
6346    Ok((peeled, symref_target, attributes))
6347}
6348
6349fn parse_protocol_v2_ls_refs_attributes<'a>(
6350    format: ObjectFormat,
6351    fields: impl Iterator<Item = &'a str>,
6352) -> Result<(Option<String>, Vec<String>)> {
6353    parse_protocol_v2_ls_refs_attributes_with(format, fields, |attr| {
6354        if attr.starts_with("peeled:") {
6355            return Err(GitError::InvalidFormat(
6356                "ls-refs unborn line has peeled attribute".into(),
6357            ));
6358        }
6359        Ok(false)
6360    })
6361}
6362
6363fn parse_protocol_v2_ls_refs_attributes_with<'a, F>(
6364    _format: ObjectFormat,
6365    fields: impl Iterator<Item = &'a str>,
6366    mut handle_known: F,
6367) -> Result<(Option<String>, Vec<String>)>
6368where
6369    F: FnMut(&str) -> Result<bool>,
6370{
6371    let mut symref_target = None;
6372    let mut attributes = Vec::new();
6373    for attr in fields {
6374        validate_protocol_v2_token("ls-refs attribute", attr)?;
6375        if let Some(value) = attr.strip_prefix("symref-target:") {
6376            if symref_target.is_some() {
6377                return Err(GitError::InvalidFormat(
6378                    "ls-refs response has duplicate symref-target attribute".into(),
6379                ));
6380            }
6381            validate_protocol_v2_token("ls-refs symref-target", value)?;
6382            symref_target = Some(value.to_string());
6383        } else if !handle_known(attr)? {
6384            attributes.push(attr.to_string());
6385        }
6386    }
6387    Ok((symref_target, attributes))
6388}
6389
6390fn format_protocol_v2_ls_refs_record(record: &ProtocolV2LsRefsRecord) -> Result<String> {
6391    let mut out = String::new();
6392    match record {
6393        ProtocolV2LsRefsRecord::Ref(reference) => {
6394            validate_protocol_v2_token("ls-refs ref name", &reference.name)?;
6395            out.push_str(&reference.oid.to_string());
6396            out.push(' ');
6397            out.push_str(&reference.name);
6398            if let Some(peeled) = &reference.peeled {
6399                if peeled.format() != reference.oid.format() {
6400                    return Err(GitError::InvalidObjectId(
6401                        "ls-refs peeled object format does not match ref object format".into(),
6402                    ));
6403                }
6404                out.push(' ');
6405                out.push_str("peeled:");
6406                out.push_str(&peeled.to_string());
6407            }
6408            if let Some(target) = &reference.symref_target {
6409                validate_protocol_v2_token("ls-refs symref-target", target)?;
6410                out.push(' ');
6411                out.push_str("symref-target:");
6412                out.push_str(target);
6413            }
6414            append_protocol_v2_ls_refs_attributes(&mut out, &reference.attributes)?;
6415        }
6416        ProtocolV2LsRefsRecord::Unborn {
6417            name,
6418            symref_target,
6419            attributes,
6420        } => {
6421            validate_protocol_v2_token("ls-refs ref name", name)?;
6422            out.push_str("unborn ");
6423            out.push_str(name);
6424            if let Some(target) = symref_target {
6425                validate_protocol_v2_token("ls-refs symref-target", target)?;
6426                out.push(' ');
6427                out.push_str("symref-target:");
6428                out.push_str(target);
6429            }
6430            append_protocol_v2_ls_refs_attributes(&mut out, attributes)?;
6431        }
6432    }
6433    Ok(out)
6434}
6435
6436fn append_protocol_v2_ls_refs_attributes(out: &mut String, attributes: &[String]) -> Result<()> {
6437    for attr in attributes {
6438        validate_protocol_v2_token("ls-refs attribute", attr)?;
6439        if attr.starts_with("peeled:") || attr.starts_with("symref-target:") {
6440            return Err(GitError::InvalidFormat(
6441                "ls-refs generic attributes must not duplicate known attributes".into(),
6442            ));
6443        }
6444        out.push(' ');
6445        out.push_str(attr);
6446    }
6447    Ok(())
6448}
6449
6450fn parse_fetch_section_header(payload: &[u8]) -> Result<String> {
6451    let name = parse_protocol_v2_line_text("fetch response section", payload)?;
6452    validate_capability_name(name)?;
6453    Ok(name.to_string())
6454}
6455
6456fn flush_terminates_protocol_v2_response(frames: &[PktLineFrame], idx: usize) -> bool {
6457    idx + 1 == frames.len()
6458        || (idx + 2 == frames.len() && matches!(frames[idx + 1], PktLineFrame::ResponseEnd))
6459}
6460
6461fn parse_fetch_section(
6462    format: ObjectFormat,
6463    name: String,
6464    lines: Vec<Vec<u8>>,
6465) -> Result<ProtocolV2FetchResponseSection> {
6466    match name.as_str() {
6467        "acknowledgments" => lines
6468            .iter()
6469            .map(|line| parse_fetch_acknowledgment(format, line))
6470            .collect::<Result<Vec<_>>>()
6471            .map(ProtocolV2FetchResponseSection::Acknowledgments),
6472        "shallow-info" => lines
6473            .iter()
6474            .map(|line| parse_fetch_shallow_info(format, line))
6475            .collect::<Result<Vec<_>>>()
6476            .map(ProtocolV2FetchResponseSection::ShallowInfo),
6477        "wanted-refs" => lines
6478            .iter()
6479            .map(|line| parse_fetch_wanted_ref(format, line))
6480            .collect::<Result<Vec<_>>>()
6481            .map(ProtocolV2FetchResponseSection::WantedRefs),
6482        "packfile-uris" => lines
6483            .iter()
6484            .map(|line| parse_fetch_packfile_uri(format, line))
6485            .collect::<Result<Vec<_>>>()
6486            .map(ProtocolV2FetchResponseSection::PackfileUris),
6487        "packfile" => Ok(ProtocolV2FetchResponseSection::Packfile(lines)),
6488        _ => Ok(ProtocolV2FetchResponseSection::Unknown { name, lines }),
6489    }
6490}
6491
6492fn parse_fetch_acknowledgment(
6493    format: ObjectFormat,
6494    line: &[u8],
6495) -> Result<ProtocolV2FetchAcknowledgment> {
6496    let text = parse_protocol_v2_line_text("fetch acknowledgment", line)?;
6497    match text {
6498        "NAK" => Ok(ProtocolV2FetchAcknowledgment::Nak),
6499        "ready" => Ok(ProtocolV2FetchAcknowledgment::Ready),
6500        value if value.starts_with("ACK ") => Ok(ProtocolV2FetchAcknowledgment::Ack(
6501            parse_oid_argument(format, "fetch ACK", value, "ACK ")?,
6502        )),
6503        other => Err(GitError::InvalidFormat(format!(
6504            "unsupported fetch acknowledgment {other}"
6505        ))),
6506    }
6507}
6508
6509fn parse_fetch_shallow_info(
6510    format: ObjectFormat,
6511    line: &[u8],
6512) -> Result<ProtocolV2FetchShallowInfo> {
6513    let text = parse_protocol_v2_line_text("fetch shallow-info", line)?;
6514    if text.starts_with("shallow ") {
6515        return Ok(ProtocolV2FetchShallowInfo::Shallow(parse_oid_argument(
6516            format,
6517            "fetch shallow",
6518            text,
6519            "shallow ",
6520        )?));
6521    }
6522    if text.starts_with("unshallow ") {
6523        return Ok(ProtocolV2FetchShallowInfo::Unshallow(parse_oid_argument(
6524            format,
6525            "fetch unshallow",
6526            text,
6527            "unshallow ",
6528        )?));
6529    }
6530    Err(GitError::InvalidFormat(format!(
6531        "unsupported fetch shallow-info {text}"
6532    )))
6533}
6534
6535fn parse_fetch_wanted_ref(format: ObjectFormat, line: &[u8]) -> Result<ProtocolV2FetchWantedRef> {
6536    let text = parse_protocol_v2_line_text("fetch wanted-ref", line)?;
6537    let (oid, name) = text
6538        .split_once(' ')
6539        .ok_or_else(|| GitError::InvalidFormat("fetch wanted-ref is missing name".into()))?;
6540    validate_protocol_v2_token("fetch wanted-ref name", name)?;
6541    Ok(ProtocolV2FetchWantedRef {
6542        oid: ObjectId::from_hex(format, oid)?,
6543        name: name.to_string(),
6544    })
6545}
6546
6547fn parse_fetch_packfile_uri(
6548    format: ObjectFormat,
6549    line: &[u8],
6550) -> Result<ProtocolV2FetchPackfileUri> {
6551    let text = parse_protocol_v2_line_text("fetch packfile-uri", line)?;
6552    let (pack_hash, uri) = text
6553        .split_once(' ')
6554        .ok_or_else(|| GitError::InvalidFormat("fetch packfile-uri is missing uri".into()))?;
6555    validate_protocol_v2_token("fetch packfile-uri hash", pack_hash)?;
6556    validate_protocol_v2_token("fetch packfile-uri", uri)?;
6557    Ok(ProtocolV2FetchPackfileUri {
6558        pack_hash: ObjectId::from_hex(format, pack_hash)?,
6559        uri: uri.to_string(),
6560    })
6561}
6562
6563fn protocol_v2_fetch_section_name(section: &ProtocolV2FetchResponseSection) -> &str {
6564    match section {
6565        ProtocolV2FetchResponseSection::Acknowledgments(_) => "acknowledgments",
6566        ProtocolV2FetchResponseSection::ShallowInfo(_) => "shallow-info",
6567        ProtocolV2FetchResponseSection::WantedRefs(_) => "wanted-refs",
6568        ProtocolV2FetchResponseSection::PackfileUris(_) => "packfile-uris",
6569        ProtocolV2FetchResponseSection::Packfile(_) => "packfile",
6570        ProtocolV2FetchResponseSection::Unknown { name, .. } => name,
6571    }
6572}
6573
6574fn format_protocol_v2_fetch_section_lines(
6575    section: &ProtocolV2FetchResponseSection,
6576) -> Result<Vec<Vec<u8>>> {
6577    match section {
6578        ProtocolV2FetchResponseSection::Acknowledgments(acks) => acks
6579            .iter()
6580            .map(|ack| match ack {
6581                ProtocolV2FetchAcknowledgment::Nak => Ok(line_from_str("NAK")),
6582                ProtocolV2FetchAcknowledgment::Ack(oid) => Ok(line_from_str(&format!("ACK {oid}"))),
6583                ProtocolV2FetchAcknowledgment::Ready => Ok(line_from_str("ready")),
6584            })
6585            .collect(),
6586        ProtocolV2FetchResponseSection::ShallowInfo(entries) => entries
6587            .iter()
6588            .map(|entry| match entry {
6589                ProtocolV2FetchShallowInfo::Shallow(oid) => {
6590                    Ok(line_from_str(&format!("shallow {oid}")))
6591                }
6592                ProtocolV2FetchShallowInfo::Unshallow(oid) => {
6593                    Ok(line_from_str(&format!("unshallow {oid}")))
6594                }
6595            })
6596            .collect(),
6597        ProtocolV2FetchResponseSection::WantedRefs(refs) => refs
6598            .iter()
6599            .map(|wanted| {
6600                validate_protocol_v2_token("fetch wanted-ref name", &wanted.name)?;
6601                Ok(line_from_str(&format!("{} {}", wanted.oid, wanted.name)))
6602            })
6603            .collect(),
6604        ProtocolV2FetchResponseSection::PackfileUris(uris) => uris
6605            .iter()
6606            .map(|packfile_uri| {
6607                validate_protocol_v2_token("fetch packfile-uri", &packfile_uri.uri)?;
6608                Ok(line_from_str(&format!(
6609                    "{} {}",
6610                    packfile_uri.pack_hash, packfile_uri.uri
6611                )))
6612            })
6613            .collect(),
6614        ProtocolV2FetchResponseSection::Packfile(lines) => Ok(lines.clone()),
6615        ProtocolV2FetchResponseSection::Unknown { name, lines } => {
6616            validate_capability_name(name)?;
6617            for line in lines {
6618                validate_protocol_v2_line("fetch unknown section line", line)?;
6619            }
6620            Ok(lines.clone())
6621        }
6622    }
6623}
6624
6625fn parse_protocol_v2_object_info_record(
6626    format: ObjectFormat,
6627    line: &[u8],
6628) -> Result<ProtocolV2ObjectInfoRecord> {
6629    let text = parse_protocol_v2_line_text("object-info record", line)?;
6630    let mut fields = text.split(' ');
6631    let oid = fields
6632        .next()
6633        .ok_or_else(|| GitError::InvalidFormat("object-info record is missing oid".into()))?;
6634    let size = fields
6635        .next()
6636        .ok_or_else(|| GitError::InvalidFormat("object-info record is missing size".into()))?;
6637    if fields.next().is_some() {
6638        return Err(GitError::InvalidFormat(
6639            "object-info record has too many fields".into(),
6640        ));
6641    }
6642    validate_protocol_v2_token("object-info oid", oid)?;
6643    validate_protocol_v2_token("object-info size", size)?;
6644    let size = size
6645        .parse::<u64>()
6646        .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6647    Ok(ProtocolV2ObjectInfoRecord {
6648        oid: ObjectId::from_hex(format, oid)?,
6649        size,
6650    })
6651}
6652
6653fn parse_dumb_http_info_ref_record(format: ObjectFormat, line: &[u8]) -> Result<DumbHttpRefRecord> {
6654    validate_dumb_http_info_ref_line(line)?;
6655    let line = trim_trailing_lf(line);
6656    let tab = line
6657        .iter()
6658        .position(|byte| *byte == b'\t')
6659        .ok_or_else(|| GitError::InvalidFormat("dumb HTTP ref record is missing name".into()))?;
6660    let (oid, name) = (&line[..tab], &line[tab + 1..]);
6661    let oid = std::str::from_utf8(oid).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6662    validate_protocol_v2_token("dumb HTTP ref oid", oid)?;
6663    let name = std::str::from_utf8(name).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6664    let (name, peeled) = name
6665        .strip_suffix("^{}")
6666        .map_or((name, false), |name| (name, true));
6667    validate_dumb_http_ref_name(name)?;
6668    Ok(DumbHttpRefRecord {
6669        oid: ObjectId::from_hex(format, oid)?,
6670        name: name.to_string(),
6671        peeled,
6672    })
6673}
6674
6675fn parse_dumb_http_alternate(line: &[u8]) -> Result<String> {
6676    validate_dumb_http_alternate_line(line)?;
6677    let line = trim_trailing_lf(line);
6678    let alternate =
6679        std::str::from_utf8(line).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6680    validate_dumb_http_alternate(alternate)?;
6681    Ok(alternate.to_string())
6682}
6683
6684fn parse_dumb_http_pack_record(format: ObjectFormat, line: &[u8]) -> Result<DumbHttpPackRecord> {
6685    validate_dumb_http_info_ref_line(line)?;
6686    let line = parse_protocol_v2_line_text("dumb HTTP pack record", line)?;
6687    let pack_name = line
6688        .strip_prefix("P ")
6689        .ok_or_else(|| GitError::InvalidFormat("dumb HTTP pack record must start with P".into()))?;
6690    let hash = pack_name
6691        .strip_prefix("pack-")
6692        .and_then(|value| value.strip_suffix(".pack"))
6693        .ok_or_else(|| GitError::InvalidFormat("invalid dumb HTTP pack name".into()))?;
6694    validate_protocol_v2_token("dumb HTTP pack hash", hash)?;
6695    Ok(DumbHttpPackRecord {
6696        hash: ObjectId::from_hex(format, hash)?,
6697    })
6698}
6699
6700fn encode_protocol_v2_capability(capability: &Capability) -> Result<Vec<u8>> {
6701    validate_capability_name(&capability.name)?;
6702    let mut out = capability.name.as_bytes().to_vec();
6703    if let Some(value) = &capability.value {
6704        validate_protocol_v2_capability_value(value)?;
6705        out.push(b'=');
6706        out.extend_from_slice(value.as_bytes());
6707    }
6708    Ok(out)
6709}
6710
6711fn validate_capability_field(label: &str, value: &str) -> Result<()> {
6712    if value.is_empty() {
6713        return Err(GitError::InvalidFormat(format!("{label} is empty")));
6714    }
6715    if value
6716        .bytes()
6717        .any(|byte| matches!(byte, b' ' | b'\n' | b'\r' | b'\t' | 0))
6718    {
6719        return Err(GitError::InvalidFormat(format!(
6720            "{label} contains a delimiter byte"
6721        )));
6722    }
6723    Ok(())
6724}
6725
6726fn validate_capability_name(value: &str) -> Result<()> {
6727    validate_capability_field("capability name", value)?;
6728    if value.bytes().any(|byte| byte == b'=') {
6729        return Err(GitError::InvalidFormat(
6730            "capability name contains a delimiter byte".into(),
6731        ));
6732    }
6733    Ok(())
6734}
6735
6736fn validate_protocol_v2_capability_value(value: &str) -> Result<()> {
6737    if value.is_empty() {
6738        return Err(GitError::InvalidFormat(
6739            "protocol v2 capability value is empty".into(),
6740        ));
6741    }
6742    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6743        return Err(GitError::InvalidFormat(
6744            "protocol v2 capability value contains a delimiter byte".into(),
6745        ));
6746    }
6747    Ok(())
6748}
6749
6750fn validate_protocol_v2_argument(value: &[u8]) -> Result<()> {
6751    if value.is_empty() {
6752        return Err(GitError::InvalidFormat(
6753            "protocol v2 command argument is empty".into(),
6754        ));
6755    }
6756    if value.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
6757        return Err(GitError::InvalidFormat(
6758            "protocol v2 command argument contains a delimiter byte".into(),
6759        ));
6760    }
6761    Ok(())
6762}
6763
6764fn validate_upload_archive_argument(value: &str) -> Result<()> {
6765    if value.is_empty() {
6766        return Err(GitError::InvalidFormat(
6767            "upload-archive argument is empty".into(),
6768        ));
6769    }
6770    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6771        return Err(GitError::InvalidFormat(
6772            "upload-archive argument contains a delimiter byte".into(),
6773        ));
6774    }
6775    Ok(())
6776}
6777
6778fn validate_upload_archive_status_message(value: &str) -> Result<()> {
6779    if value.is_empty() {
6780        return Err(GitError::InvalidFormat(
6781            "upload-archive status message is empty".into(),
6782        ));
6783    }
6784    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6785        return Err(GitError::InvalidFormat(
6786            "upload-archive status message contains a delimiter byte".into(),
6787        ));
6788    }
6789    Ok(())
6790}
6791
6792fn non_empty(value: &str) -> Option<&str> {
6793    (!value.is_empty()).then_some(value)
6794}
6795
6796fn validate_refspec_value(value: &str) -> Result<()> {
6797    if value.is_empty() {
6798        return Err(GitError::InvalidFormat("refspec is empty".into()));
6799    }
6800    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6801        return Err(GitError::InvalidFormat(
6802            "refspec contains a delimiter byte".into(),
6803        ));
6804    }
6805    Ok(())
6806}
6807
6808fn validate_refspec_endpoint(label: &str, value: &str) -> Result<()> {
6809    if value.is_empty() {
6810        return Err(GitError::InvalidFormat(format!("{label} is empty")));
6811    }
6812    if value
6813        .bytes()
6814        .any(|byte| matches!(byte, b':' | b' ' | b'\t' | b'\n' | b'\r' | 0))
6815    {
6816        return Err(GitError::InvalidFormat(format!(
6817            "{label} contains a delimiter byte"
6818        )));
6819    }
6820    Ok(())
6821}
6822
6823fn count_refspec_wildcards(value: &str) -> usize {
6824    value.bytes().filter(|byte| *byte == b'*').count()
6825}
6826
6827fn validate_refspec_shape(refspec: &RefSpec) -> Result<()> {
6828    if refspec.force && refspec.negative {
6829        return Err(GitError::InvalidFormat(
6830            "negative refspec must not be forced".into(),
6831        ));
6832    }
6833    if refspec.negative && refspec.dst.is_some() {
6834        return Err(GitError::InvalidFormat(
6835            "negative refspec must not have a destination".into(),
6836        ));
6837    }
6838    if refspec.negative && refspec.src.is_none() {
6839        return Err(GitError::InvalidFormat(
6840            "negative refspec is missing a source".into(),
6841        ));
6842    }
6843    if refspec.src.is_none() && refspec.dst.is_none() && refspec.negative {
6844        return Err(GitError::InvalidFormat(
6845            "refspec must include a source or destination".into(),
6846        ));
6847    }
6848    if let Some(src) = &refspec.src {
6849        validate_refspec_endpoint("refspec source", src)?;
6850    }
6851    if let Some(dst) = &refspec.dst {
6852        validate_refspec_endpoint("refspec destination", dst)?;
6853    }
6854    let src_pattern_count = refspec
6855        .src
6856        .as_deref()
6857        .map(count_refspec_wildcards)
6858        .unwrap_or(0);
6859    let dst_pattern_count = refspec
6860        .dst
6861        .as_deref()
6862        .map(count_refspec_wildcards)
6863        .unwrap_or(0);
6864    if src_pattern_count > 1 || dst_pattern_count > 1 {
6865        return Err(GitError::InvalidFormat(
6866            "refspec endpoint has too many wildcards".into(),
6867        ));
6868    }
6869    if refspec.dst.is_some() && (src_pattern_count == 1) != (dst_pattern_count == 1) {
6870        return Err(GitError::InvalidFormat(
6871            "refspec wildcard must appear in both source and destination".into(),
6872        ));
6873    }
6874    if refspec.pattern != (src_pattern_count == 1 || dst_pattern_count == 1) {
6875        return Err(GitError::InvalidFormat(
6876            "refspec pattern flag does not match endpoints".into(),
6877        ));
6878    }
6879    Ok(())
6880}
6881
6882fn parse_fetch_head_record(format: ObjectFormat, line: &[u8]) -> Result<FetchHeadRecord> {
6883    validate_fetch_head_line(line)?;
6884    let line = trim_trailing_lf(line);
6885    let mut fields = line.splitn(3, |byte| *byte == b'\t');
6886    let oid = fields
6887        .next()
6888        .ok_or_else(|| GitError::InvalidFormat("FETCH_HEAD record is missing oid".into()))?;
6889    let merge_marker = fields.next().ok_or_else(|| {
6890        GitError::InvalidFormat("FETCH_HEAD record is missing merge marker".into())
6891    })?;
6892    let description = fields.next().ok_or_else(|| {
6893        GitError::InvalidFormat("FETCH_HEAD record is missing description".into())
6894    })?;
6895    let oid = std::str::from_utf8(oid).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6896    validate_protocol_v2_token("FETCH_HEAD oid", oid)?;
6897    let not_for_merge = match merge_marker {
6898        b"" => false,
6899        b"not-for-merge" => true,
6900        _ => {
6901            return Err(GitError::InvalidFormat(
6902                "FETCH_HEAD record has invalid merge marker".into(),
6903            ));
6904        }
6905    };
6906    let description =
6907        std::str::from_utf8(description).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6908    validate_fetch_head_description_field(description)?;
6909    Ok(FetchHeadRecord {
6910        oid: ObjectId::from_hex(format, oid)?,
6911        not_for_merge,
6912        description: description.to_string(),
6913    })
6914}
6915
6916fn validate_fetch_head_line(value: &[u8]) -> Result<()> {
6917    if value.is_empty() {
6918        return Err(GitError::InvalidFormat("FETCH_HEAD record is empty".into()));
6919    }
6920    if !value.ends_with(b"\n") {
6921        return Err(GitError::InvalidFormat(
6922            "FETCH_HEAD record missing LF".into(),
6923        ));
6924    }
6925    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
6926        return Err(GitError::InvalidFormat(
6927            "FETCH_HEAD record contains a delimiter byte".into(),
6928        ));
6929    }
6930    Ok(())
6931}
6932
6933fn validate_fetch_head_description_field(value: &str) -> Result<()> {
6934    if value.is_empty() {
6935        return Err(GitError::InvalidFormat(
6936            "FETCH_HEAD description is empty".into(),
6937        ));
6938    }
6939    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6940        return Err(GitError::InvalidFormat(
6941            "FETCH_HEAD description contains a delimiter byte".into(),
6942        ));
6943    }
6944    Ok(())
6945}
6946
6947fn refspec_is_excluded(negative: &[&RefSpec], source: &str) -> Result<bool> {
6948    for refspec in negative {
6949        if refspec_matches_source(refspec, source)? {
6950            return Ok(true);
6951        }
6952    }
6953    Ok(false)
6954}
6955
6956fn zero_object_id(format: ObjectFormat) -> Result<ObjectId> {
6957    Ok(ObjectId::null(format))
6958}
6959
6960fn local_ref<'a>(refs: &'a [PushSourceRef], name: &str) -> Option<&'a PushSourceRef> {
6961    refs.iter().find(|reference| reference.name == name)
6962}
6963
6964fn remote_ref<'a>(refs: &'a [RefAdvertisement], name: &str) -> Option<&'a RefAdvertisement> {
6965    refs.iter().find(|reference| reference.name == name)
6966}
6967
6968fn validate_push_source_ref(format: ObjectFormat, reference: &PushSourceRef) -> Result<()> {
6969    if reference.oid.format() != format {
6970        return Err(GitError::InvalidObjectId(
6971            "push source ref object format does not match repository".into(),
6972        ));
6973    }
6974    validate_refspec_endpoint("push source ref name", &reference.name)
6975}
6976
6977fn require_receive_pack_feature(advertised: bool, name: &str) -> Result<()> {
6978    if advertised {
6979        Ok(())
6980    } else {
6981        Err(GitError::InvalidFormat(format!(
6982            "receive-pack feature {name} was not advertised"
6983        )))
6984    }
6985}
6986
6987fn validate_smart_http_service(service: GitService) -> Result<()> {
6988    match service {
6989        GitService::UploadPack | GitService::ReceivePack => Ok(()),
6990        GitService::UploadArchive => Err(GitError::InvalidFormat(
6991            "smart HTTP only supports upload-pack and receive-pack services".into(),
6992        )),
6993    }
6994}
6995
6996fn normalize_http_repository_path(path: &str) -> Result<String> {
6997    if path.is_empty() {
6998        return Err(GitError::InvalidFormat(
6999            "smart HTTP repository path is empty".into(),
7000        ));
7001    }
7002    if !path.starts_with('/') {
7003        return Err(GitError::InvalidFormat(
7004            "smart HTTP repository path must start with /".into(),
7005        ));
7006    }
7007    if path
7008        .bytes()
7009        .any(|byte| matches!(byte, b'\n' | b'\r' | 0 | b'?' | b'#'))
7010    {
7011        return Err(GitError::InvalidFormat(
7012            "smart HTTP repository path contains a delimiter byte".into(),
7013        ));
7014    }
7015    let normalized = path.trim_end_matches('/');
7016    Ok(if normalized.is_empty() {
7017        "/".into()
7018    } else {
7019        normalized.to_string()
7020    })
7021}
7022
7023fn dumb_http_pack_resource_path(
7024    repository_path: &str,
7025    hash: &ObjectId,
7026    suffix: &str,
7027) -> Result<String> {
7028    let repository_path = normalize_http_repository_path(repository_path)?;
7029    Ok(format!(
7030        "{repository_path}/objects/pack/pack-{hash}.{suffix}"
7031    ))
7032}
7033
7034fn parse_smart_http_content_type(value: &str, suffix: &str) -> Result<GitService> {
7035    let value = value.trim();
7036    if value.is_empty() {
7037        return Err(GitError::InvalidFormat(
7038            "smart HTTP content type is empty".into(),
7039        ));
7040    }
7041    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
7042        return Err(GitError::InvalidFormat(
7043            "smart HTTP content type contains a delimiter byte".into(),
7044        ));
7045    }
7046    let value = value.to_ascii_lowercase();
7047    let service = value
7048        .strip_prefix("application/x-")
7049        .and_then(|value| value.strip_suffix(suffix))
7050        .ok_or_else(|| GitError::InvalidFormat("invalid smart HTTP content type".into()))?;
7051    let service = parse_git_service(service)?;
7052    validate_smart_http_service(service)?;
7053    Ok(service)
7054}
7055
7056fn validate_dumb_http_info_ref_line(value: &[u8]) -> Result<()> {
7057    if value.is_empty() {
7058        return Err(GitError::InvalidFormat(
7059            "dumb HTTP ref record is empty".into(),
7060        ));
7061    }
7062    if !value.ends_with(b"\n") {
7063        return Err(GitError::InvalidFormat(
7064            "dumb HTTP ref record missing LF".into(),
7065        ));
7066    }
7067    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7068        return Err(GitError::InvalidFormat(
7069            "dumb HTTP ref record contains a delimiter byte".into(),
7070        ));
7071    }
7072    Ok(())
7073}
7074
7075fn validate_dumb_http_ref_name(value: &str) -> Result<()> {
7076    validate_protocol_v2_token("dumb HTTP ref name", value)?;
7077    if value.ends_with("^{}") {
7078        return Err(GitError::InvalidFormat(
7079            "dumb HTTP ref name must not include peeled suffix".into(),
7080        ));
7081    }
7082    Ok(())
7083}
7084
7085fn validate_dumb_http_alternate_line(value: &[u8]) -> Result<()> {
7086    if value.is_empty() {
7087        return Err(GitError::InvalidFormat(
7088            "dumb HTTP alternate is empty".into(),
7089        ));
7090    }
7091    if !value.ends_with(b"\n") {
7092        return Err(GitError::InvalidFormat(
7093            "dumb HTTP alternate missing LF".into(),
7094        ));
7095    }
7096    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7097        return Err(GitError::InvalidFormat(
7098            "dumb HTTP alternate contains a delimiter byte".into(),
7099        ));
7100    }
7101    Ok(())
7102}
7103
7104fn validate_dumb_http_alternate(value: &str) -> Result<()> {
7105    if value.is_empty() {
7106        return Err(GitError::InvalidFormat(
7107            "dumb HTTP alternate is empty".into(),
7108        ));
7109    }
7110    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
7111        return Err(GitError::InvalidFormat(
7112            "dumb HTTP alternate contains a delimiter byte".into(),
7113        ));
7114    }
7115    Ok(())
7116}
7117
7118fn validate_protocol_v2_token(label: &str, value: &str) -> Result<()> {
7119    if value.is_empty() {
7120        return Err(GitError::InvalidFormat(format!("{label} is empty")));
7121    }
7122    if value
7123        .bytes()
7124        .any(|byte| matches!(byte, b' ' | b'\n' | b'\r' | 0))
7125    {
7126        return Err(GitError::InvalidFormat(format!(
7127            "{label} contains a delimiter byte"
7128        )));
7129    }
7130    Ok(())
7131}
7132
7133fn validate_protocol_v2_line(label: &str, value: &[u8]) -> Result<()> {
7134    if value.is_empty() {
7135        return Err(GitError::InvalidFormat(format!("{label} is empty")));
7136    }
7137    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7138        return Err(GitError::InvalidFormat(format!(
7139            "{label} contains a delimiter byte"
7140        )));
7141    }
7142    Ok(())
7143}
7144
7145fn parse_protocol_v2_line_text<'a>(label: &str, value: &'a [u8]) -> Result<&'a str> {
7146    validate_protocol_v2_line(label, value)?;
7147    let value = trim_trailing_lf(value);
7148    if value.is_empty() {
7149        return Err(GitError::InvalidFormat(format!("{label} is empty")));
7150    }
7151    if value.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
7152        return Err(GitError::InvalidFormat(format!(
7153            "{label} contains a delimiter byte"
7154        )));
7155    }
7156    std::str::from_utf8(value).map_err(|err| GitError::InvalidFormat(err.to_string()))
7157}
7158
7159fn parse_oid_argument(
7160    format: ObjectFormat,
7161    label: &str,
7162    value: &str,
7163    prefix: &str,
7164) -> Result<ObjectId> {
7165    let oid = value
7166        .strip_prefix(prefix)
7167        .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
7168    validate_protocol_v2_token(label, oid)?;
7169    ObjectId::from_hex(format, oid)
7170}
7171
7172fn parse_u32_argument(label: &str, value: &str, prefix: &str) -> Result<u32> {
7173    let number = value
7174        .strip_prefix(prefix)
7175        .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
7176    validate_protocol_v2_token(label, number)?;
7177    let parsed = number
7178        .parse::<u32>()
7179        .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
7180    if parsed == 0 {
7181        return Err(GitError::InvalidFormat(format!("{label} must be positive")));
7182    }
7183    Ok(parsed)
7184}
7185
7186fn parse_u64_argument(label: &str, value: &str, prefix: &str) -> Result<u64> {
7187    let number = value
7188        .strip_prefix(prefix)
7189        .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
7190    validate_protocol_v2_token(label, number)?;
7191    number
7192        .parse::<u64>()
7193        .map_err(|err| GitError::InvalidFormat(err.to_string()))
7194}
7195
7196fn line(mut payload: Vec<u8>) -> Vec<u8> {
7197    payload.push(b'\n');
7198    payload
7199}
7200
7201fn line_from_str(payload: &str) -> Vec<u8> {
7202    line(payload.as_bytes().to_vec())
7203}
7204
7205fn trim_trailing_lf(input: &[u8]) -> &[u8] {
7206    input.strip_suffix(b"\n").unwrap_or(input)
7207}
7208
7209#[cfg(test)]
7210mod tests {
7211    use super::*;
7212
7213    #[test]
7214    fn pkt_line_frame_encodes_data_and_control_frames() {
7215        assert_eq!(PktLine(b"hello\n".to_vec()).encode(), b"000ahello\n");
7216        assert_eq!(
7217            PktLineFrame::data(b"hello\n".to_vec())
7218                .expect("test operation should succeed")
7219                .encode(),
7220            b"000ahello\n"
7221        );
7222        assert_eq!(PktLineFrame::Flush.encode(), b"0000");
7223        assert_eq!(PktLineFrame::Delimiter.encode(), b"0001");
7224        assert_eq!(PktLineFrame::ResponseEnd.encode(), b"0002");
7225        assert_eq!(
7226            PktLineFrame::data(b"hello\n".to_vec())
7227                .expect("test operation should succeed")
7228                .try_encode()
7229                .expect("test operation should succeed"),
7230            b"000ahello\n"
7231        );
7232    }
7233
7234    #[test]
7235    fn pkt_line_frame_parses_data_and_control_frames() {
7236        assert_eq!(
7237            PktLineFrame::parse(b"000ahello\n").expect("test operation should succeed"),
7238            (PktLineFrame::Data(b"hello\n".to_vec()), 10)
7239        );
7240        assert_eq!(
7241            PktLineFrame::parse(b"0000").expect("test operation should succeed"),
7242            (PktLineFrame::Flush, 4)
7243        );
7244        assert_eq!(
7245            PktLineFrame::parse(b"0001").expect("test operation should succeed"),
7246            (PktLineFrame::Delimiter, 4)
7247        );
7248        assert_eq!(
7249            PktLineFrame::parse(b"0002").expect("test operation should succeed"),
7250            (PktLineFrame::ResponseEnd, 4)
7251        );
7252    }
7253
7254    #[test]
7255    fn pkt_line_stream_parses_multiple_frames() {
7256        let frames = parse_pkt_line_stream(b"000eversion 2\n00010009done\n0000")
7257            .expect("test operation should succeed");
7258        assert_eq!(
7259            frames,
7260            vec![
7261                PktLineFrame::Data(b"version 2\n".to_vec()),
7262                PktLineFrame::Delimiter,
7263                PktLineFrame::Data(b"done\n".to_vec()),
7264                PktLineFrame::Flush,
7265            ]
7266        );
7267    }
7268
7269    #[test]
7270    fn pkt_line_stream_reads_and_writes_incremental_io() {
7271        let frames = vec![
7272            PktLineFrame::Data(b"version 2\n".to_vec()),
7273            PktLineFrame::Delimiter,
7274            PktLineFrame::Data(b"done\n".to_vec()),
7275            PktLineFrame::Flush,
7276        ];
7277        let mut encoded = Vec::new();
7278        write_pkt_line_frames(&mut encoded, &frames).expect("test operation should succeed");
7279        assert_eq!(encoded, b"000eversion 2\n00010009done\n0000");
7280        assert_eq!(
7281            read_pkt_line_frames(&mut encoded.as_slice()).expect("test operation should succeed"),
7282            frames
7283        );
7284
7285        let mut empty: &[u8] = b"";
7286        assert_eq!(
7287            read_pkt_line_frame(&mut empty).expect("test operation should succeed"),
7288            None
7289        );
7290    }
7291
7292    #[test]
7293    fn pkt_line_stream_reads_until_control_packets() {
7294        let input = b"000eversion 2\n0000trailing";
7295        let frames = read_pkt_line_frames_until_flush(&mut &input[..])
7296            .expect("test operation should succeed");
7297        assert_eq!(
7298            frames,
7299            vec![
7300                PktLineFrame::Data(b"version 2\n".to_vec()),
7301                PktLineFrame::Flush,
7302            ]
7303        );
7304
7305        let input = b"0009done\n0002next";
7306        let frames = read_pkt_line_frames_until_response_end(&mut &input[..])
7307            .expect("test operation should succeed");
7308        assert_eq!(
7309            frames,
7310            vec![
7311                PktLineFrame::Data(b"done\n".to_vec()),
7312                PktLineFrame::ResponseEnd,
7313            ]
7314        );
7315        assert!(read_pkt_line_frames_until_flush(&mut &b"0009done\n"[..]).is_err());
7316    }
7317
7318    #[test]
7319    fn pkt_line_rejects_invalid_lengths() {
7320        assert!(PktLineFrame::parse(b"000").is_err());
7321        assert!(PktLineFrame::parse(b"0003").is_err());
7322        assert!(PktLineFrame::parse(b"000ahello").is_err());
7323        assert!(PktLineFrame::parse(b"zzzz").is_err());
7324        assert!(read_pkt_line_frame(&mut &b"000"[..]).is_err());
7325        assert!(read_pkt_line_frame(&mut &b"0003"[..]).is_err());
7326    }
7327
7328    #[test]
7329    fn pkt_line_rejects_oversized_data() {
7330        let payload = vec![b'x'; PKT_LINE_MAX_PAYLOAD_LEN + 1];
7331        assert!(PktLineFrame::data(payload.clone()).is_err());
7332        assert!(PktLine(payload.clone()).try_encode().is_err());
7333        assert!(PktLineFrame::Data(payload.clone()).try_encode().is_err());
7334        assert!(write_pkt_line_frame(&mut Vec::new(), &PktLineFrame::Data(payload)).is_err());
7335        assert!(PktLineFrame::parse(b"fff1").is_err());
7336    }
7337
7338    #[test]
7339    fn protocol_error_lines_parse_encode_and_stream() {
7340        let error = parse_error_line(b"ERR remote rejected request\n")
7341            .expect("test operation should succeed");
7342        assert_eq!(
7343            error,
7344            ProtocolErrorLine {
7345                message: "remote rejected request".into(),
7346            }
7347        );
7348        assert_eq!(
7349            encode_error_line(&error).expect("test operation should succeed"),
7350            b"ERR remote rejected request\n"
7351        );
7352        assert_eq!(
7353            parse_error_frame(&PktLineFrame::Data(
7354                b"ERR remote rejected request\n".to_vec()
7355            ))
7356            .expect("test operation should succeed"),
7357            Some(error.clone())
7358        );
7359        assert_eq!(
7360            parse_error_frame(&PktLineFrame::Data(b"NAK\n".to_vec()))
7361                .expect("test operation should succeed"),
7362            None
7363        );
7364
7365        let mut encoded = Vec::new();
7366        write_error_line(&mut encoded, &error).expect("test operation should succeed");
7367        encoded.extend_from_slice(b"tail");
7368        let mut input = encoded.as_slice();
7369        assert_eq!(
7370            read_error_line(&mut input).expect("test operation should succeed"),
7371            error
7372        );
7373        assert_eq!(input, b"tail");
7374    }
7375
7376    #[test]
7377    fn protocol_error_lines_reject_malformed_messages() {
7378        assert!(parse_error_line(b"ERR\n").is_err());
7379        assert!(parse_error_line(b"ERR \n").is_err());
7380        assert!(parse_error_line(b"ERR bad\0message\n").is_err());
7381        assert!(parse_error_line(b"NAK\n").is_err());
7382        assert!(
7383            encode_error_line(&ProtocolErrorLine {
7384                message: "bad\nmessage".into(),
7385            })
7386            .is_err()
7387        );
7388        assert!(read_error_line(&mut &b"0000"[..]).is_err());
7389    }
7390
7391    #[test]
7392    fn refspec_parser_handles_fetch_push_and_negative_forms() {
7393        assert_eq!(
7394            parse_refspec("+refs/heads/*:refs/remotes/origin/*")
7395                .expect("test operation should succeed"),
7396            RefSpec {
7397                force: true,
7398                negative: false,
7399                src: Some("refs/heads/*".into()),
7400                dst: Some("refs/remotes/origin/*".into()),
7401                pattern: true,
7402            }
7403        );
7404        assert_eq!(
7405            parse_refspec("refs/heads/main").expect("test operation should succeed"),
7406            RefSpec {
7407                force: false,
7408                negative: false,
7409                src: Some("refs/heads/main".into()),
7410                dst: None,
7411                pattern: false,
7412            }
7413        );
7414        assert_eq!(
7415            parse_refspec(":refs/heads/topic").expect("test operation should succeed"),
7416            RefSpec {
7417                force: false,
7418                negative: false,
7419                src: None,
7420                dst: Some("refs/heads/topic".into()),
7421                pattern: false,
7422            }
7423        );
7424        assert_eq!(
7425            parse_refspec(":").expect("test operation should succeed"),
7426            RefSpec {
7427                force: false,
7428                negative: false,
7429                src: None,
7430                dst: None,
7431                pattern: false,
7432            }
7433        );
7434        assert_eq!(
7435            parse_refspec("^refs/tags/private/*").expect("test operation should succeed"),
7436            RefSpec {
7437                force: false,
7438                negative: true,
7439                src: Some("refs/tags/private/*".into()),
7440                dst: None,
7441                pattern: true,
7442            }
7443        );
7444    }
7445
7446    #[test]
7447    fn refspec_encode_and_map_sources() {
7448        let pattern = parse_refspec("+refs/heads/*:refs/remotes/origin/*")
7449            .expect("test operation should succeed");
7450        assert_eq!(
7451            encode_refspec(&pattern).expect("test operation should succeed"),
7452            "+refs/heads/*:refs/remotes/origin/*"
7453        );
7454        assert!(
7455            refspec_matches_source(&pattern, "refs/heads/main")
7456                .expect("test operation should succeed")
7457        );
7458        assert_eq!(
7459            refspec_map_source(&pattern, "refs/heads/main").expect("test operation should succeed"),
7460            Some("refs/remotes/origin/main".into())
7461        );
7462        assert_eq!(
7463            refspec_map_source(&pattern, "refs/tags/v1").expect("test operation should succeed"),
7464            None
7465        );
7466
7467        let direct = parse_refspec("HEAD:refs/heads/main").expect("test operation should succeed");
7468        assert_eq!(
7469            encode_refspec(&direct).expect("test operation should succeed"),
7470            "HEAD:refs/heads/main"
7471        );
7472        assert_eq!(
7473            refspec_map_source(&direct, "HEAD").expect("test operation should succeed"),
7474            Some("refs/heads/main".into())
7475        );
7476
7477        let delete = parse_refspec(":refs/heads/old").expect("test operation should succeed");
7478        assert_eq!(
7479            encode_refspec(&delete).expect("test operation should succeed"),
7480            ":refs/heads/old"
7481        );
7482        assert_eq!(
7483            refspec_map_source(&delete, "HEAD").expect("test operation should succeed"),
7484            None
7485        );
7486
7487        let matching = parse_refspec(":").expect("test operation should succeed");
7488        assert_eq!(
7489            encode_refspec(&matching).expect("test operation should succeed"),
7490            ":"
7491        );
7492    }
7493
7494    #[test]
7495    fn refspec_parser_rejects_malformed_values() {
7496        assert!(parse_refspec("").is_err());
7497        assert!(parse_refspec("+^refs/heads/main").is_err());
7498        assert!(parse_refspec("^refs/heads/main:refs/remotes/origin/main").is_err());
7499        assert!(parse_refspec("refs/heads/*:refs/remotes/origin/main").is_err());
7500        assert!(parse_refspec("refs/heads/**:refs/remotes/origin/*").is_err());
7501        assert!(parse_refspec("refs/heads/main:refs/remotes/origin/main:extra").is_err());
7502        assert!(parse_refspec("refs/heads/main\n").is_err());
7503        assert!(
7504            encode_refspec(&RefSpec {
7505                force: false,
7506                negative: false,
7507                src: Some("refs/heads/*".into()),
7508                dst: Some("refs/remotes/origin/main".into()),
7509                pattern: true,
7510            })
7511            .is_err()
7512        );
7513    }
7514
7515    #[test]
7516    fn fetch_head_records_parse_encode_and_describe_refs() {
7517        let first = ObjectId::from_hex(
7518            ObjectFormat::Sha1,
7519            "1111111111111111111111111111111111111111",
7520        )
7521        .expect("test operation should succeed");
7522        let second = ObjectId::from_hex(
7523            ObjectFormat::Sha1,
7524            "2222222222222222222222222222222222222222",
7525        )
7526        .expect("test operation should succeed");
7527        let input = b"1111111111111111111111111111111111111111\t\tbranch 'main' of ../bundle.bdl\n2222222222222222222222222222222222222222\tnot-for-merge\ttag 'v1' of ../bundle.bdl\n";
7528        let records =
7529            parse_fetch_head(ObjectFormat::Sha1, input).expect("test operation should succeed");
7530        assert_eq!(
7531            records,
7532            vec![
7533                FetchHeadRecord {
7534                    oid: first,
7535                    not_for_merge: false,
7536                    description: "branch 'main' of ../bundle.bdl".into(),
7537                },
7538                FetchHeadRecord {
7539                    oid: second,
7540                    not_for_merge: true,
7541                    description: "tag 'v1' of ../bundle.bdl".into(),
7542                },
7543            ]
7544        );
7545        assert_eq!(
7546            encode_fetch_head(&records).expect("test operation should succeed"),
7547            input
7548        );
7549        assert_eq!(
7550            parse_fetch_head(ObjectFormat::Sha1, b"").expect("test operation should succeed"),
7551            Vec::<FetchHeadRecord>::new()
7552        );
7553        assert_eq!(
7554            fetch_head_remote_description("refs/heads/main", "../bundle.bdl")
7555                .expect("test operation should succeed"),
7556            "branch 'main' of ../bundle.bdl"
7557        );
7558        assert_eq!(
7559            fetch_head_remote_description("refs/tags/v1", "../bundle.bdl")
7560                .expect("test operation should succeed"),
7561            "tag 'v1' of ../bundle.bdl"
7562        );
7563        // A bare `HEAD` fetch records just the URL — git emits an empty note.
7564        assert_eq!(
7565            fetch_head_remote_description("HEAD", "../bundle.bdl")
7566                .expect("test operation should succeed"),
7567            "../bundle.bdl"
7568        );
7569    }
7570
7571    #[test]
7572    fn fetch_head_records_streams_round_trip() {
7573        let records = vec![FetchHeadRecord {
7574            oid: ObjectId::from_hex(
7575                ObjectFormat::Sha1,
7576                "1111111111111111111111111111111111111111",
7577            )
7578            .expect("test operation should succeed"),
7579            not_for_merge: false,
7580            description: "branch 'main' of ../bundle.bdl".into(),
7581        }];
7582        let mut encoded = Vec::new();
7583        write_fetch_head(&mut encoded, &records).expect("test operation should succeed");
7584        let mut input = encoded.as_slice();
7585        assert_eq!(
7586            read_fetch_head(ObjectFormat::Sha1, &mut input).expect("test operation should succeed"),
7587            records
7588        );
7589        assert!(input.is_empty());
7590    }
7591
7592    #[test]
7593    fn fetch_head_records_reject_malformed_lines() {
7594        assert!(
7595            parse_fetch_head(
7596                ObjectFormat::Sha1,
7597                b"1111111111111111111111111111111111111111\t\tbranch 'main'"
7598            )
7599            .is_err()
7600        );
7601        assert!(
7602            parse_fetch_head(
7603                ObjectFormat::Sha1,
7604                b"1111111111111111111111111111111111111111\tfor-merge\tbranch 'main'\n"
7605            )
7606            .is_err()
7607        );
7608        assert!(parse_fetch_head(ObjectFormat::Sha1, b"not-a-hash\t\tbranch 'main'\n").is_err());
7609        assert!(
7610            encode_fetch_head(&[FetchHeadRecord {
7611                oid: ObjectId::from_hex(
7612                    ObjectFormat::Sha1,
7613                    "1111111111111111111111111111111111111111"
7614                )
7615                .expect("test operation should succeed"),
7616                not_for_merge: false,
7617                description: "bad\ndescription".into(),
7618            }])
7619            .is_err()
7620        );
7621    }
7622
7623    #[test]
7624    fn fetch_planner_maps_direct_pattern_and_negative_refspecs() {
7625        let main = ObjectId::from_hex(
7626            ObjectFormat::Sha1,
7627            "1111111111111111111111111111111111111111",
7628        )
7629        .expect("test operation should succeed");
7630        let next = ObjectId::from_hex(
7631            ObjectFormat::Sha1,
7632            "2222222222222222222222222222222222222222",
7633        )
7634        .expect("test operation should succeed");
7635        let refs = vec![
7636            RefAdvertisement {
7637                oid: main.clone(),
7638                name: "refs/heads/main".into(),
7639                capabilities: Vec::new(),
7640            },
7641            RefAdvertisement {
7642                oid: next.clone(),
7643                name: "refs/heads/tmp".into(),
7644                capabilities: Vec::new(),
7645            },
7646        ];
7647        let refspecs = vec![
7648            parse_refspec("+refs/heads/*:refs/remotes/origin/*")
7649                .expect("test operation should succeed"),
7650            parse_refspec("^refs/heads/tmp").expect("test operation should succeed"),
7651        ];
7652        assert_eq!(
7653            plan_fetch_ref_updates(&refs, &refspecs, false).expect("test operation should succeed"),
7654            vec![FetchRefUpdate {
7655                src: "refs/heads/main".into(),
7656                dst: Some("refs/remotes/origin/main".into()),
7657                oid: main,
7658                not_for_merge: false,
7659            }]
7660        );
7661    }
7662
7663    #[test]
7664    fn fetch_planner_autofollows_tags_and_builds_fetch_head_records() {
7665        let commit = ObjectId::from_hex(
7666            ObjectFormat::Sha1,
7667            "1111111111111111111111111111111111111111",
7668        )
7669        .expect("test operation should succeed");
7670        let refs = vec![
7671            RefAdvertisement {
7672                oid: commit.clone(),
7673                name: "refs/heads/main".into(),
7674                capabilities: Vec::new(),
7675            },
7676            RefAdvertisement {
7677                oid: commit.clone(),
7678                name: "refs/tags/v1".into(),
7679                capabilities: Vec::new(),
7680            },
7681        ];
7682        let refspecs = vec![
7683            parse_refspec("refs/heads/main:refs/heads/main")
7684                .expect("test operation should succeed"),
7685        ];
7686        let updates =
7687            plan_fetch_ref_updates(&refs, &refspecs, true).expect("test operation should succeed");
7688        assert_eq!(
7689            updates,
7690            vec![
7691                FetchRefUpdate {
7692                    src: "refs/heads/main".into(),
7693                    dst: Some("refs/heads/main".into()),
7694                    oid: commit.clone(),
7695                    not_for_merge: false,
7696                },
7697                FetchRefUpdate {
7698                    src: "refs/tags/v1".into(),
7699                    dst: Some("refs/tags/v1".into()),
7700                    oid: commit.clone(),
7701                    not_for_merge: true,
7702                },
7703            ]
7704        );
7705        assert_eq!(
7706            fetch_ref_updates_to_fetch_head(&updates, "../bundle.bdl")
7707                .expect("test operation should succeed"),
7708            vec![
7709                FetchHeadRecord {
7710                    oid: commit.clone(),
7711                    not_for_merge: false,
7712                    description: "branch 'main' of ../bundle.bdl".into(),
7713                },
7714                FetchHeadRecord {
7715                    oid: commit,
7716                    not_for_merge: true,
7717                    description: "tag 'v1' of ../bundle.bdl".into(),
7718                },
7719            ]
7720        );
7721    }
7722
7723    #[test]
7724    fn fetch_planner_rejects_missing_or_sourceless_refspecs() {
7725        let refs = vec![RefAdvertisement {
7726            oid: ObjectId::from_hex(
7727                ObjectFormat::Sha1,
7728                "1111111111111111111111111111111111111111",
7729            )
7730            .expect("test operation should succeed"),
7731            name: "refs/heads/main".into(),
7732            capabilities: Vec::new(),
7733        }];
7734        assert!(
7735            plan_fetch_ref_updates(
7736                &refs,
7737                &[parse_refspec("refs/heads/missing").expect("test operation should succeed")],
7738                false
7739            )
7740            .is_err()
7741        );
7742        assert!(
7743            plan_fetch_ref_updates(
7744                &refs,
7745                &[parse_refspec(":refs/heads/main").expect("test operation should succeed")],
7746                false
7747            )
7748            .is_err()
7749        );
7750    }
7751
7752    #[test]
7753    fn fetch_planner_sourceless_positive_refspec_returns_err_not_panic() {
7754        // Regression guard for the sley#7 conversion at the `let Some(src) = ..`
7755        // binding: a non-pattern positive refspec with no source must return an
7756        // error, never panic. Construct the malformed RefSpec directly so the
7757        // test pins the converted guard rather than parse_refspec's behavior.
7758        let refs = vec![RefAdvertisement {
7759            oid: ObjectId::from_hex(
7760                ObjectFormat::Sha1,
7761                "1111111111111111111111111111111111111111",
7762            )
7763            .expect("test operation should succeed"),
7764            name: "refs/heads/main".into(),
7765            capabilities: Vec::new(),
7766        }];
7767        let malformed = RefSpec {
7768            force: false,
7769            negative: false,
7770            src: None,
7771            dst: Some("refs/heads/main".into()),
7772            pattern: false,
7773        };
7774        let result = plan_fetch_ref_updates(&refs, &[malformed], false);
7775        assert!(
7776            result.is_err(),
7777            "sourceless positive refspec must yield Err, got {result:?}"
7778        );
7779    }
7780
7781    #[test]
7782    fn push_planner_builds_create_update_delete_and_matching_commands() {
7783        let old = ObjectId::from_hex(
7784            ObjectFormat::Sha1,
7785            "1111111111111111111111111111111111111111",
7786        )
7787        .expect("test operation should succeed");
7788        let new = ObjectId::from_hex(
7789            ObjectFormat::Sha1,
7790            "2222222222222222222222222222222222222222",
7791        )
7792        .expect("test operation should succeed");
7793        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
7794        let local_refs = vec![
7795            PushSourceRef {
7796                oid: new.clone(),
7797                name: "refs/heads/main".into(),
7798            },
7799            PushSourceRef {
7800                oid: new.clone(),
7801                name: "refs/heads/new".into(),
7802            },
7803        ];
7804        let remote_refs = vec![
7805            RefAdvertisement {
7806                oid: old.clone(),
7807                name: "refs/heads/main".into(),
7808                capabilities: Vec::new(),
7809            },
7810            RefAdvertisement {
7811                oid: old.clone(),
7812                name: "refs/heads/old".into(),
7813                capabilities: Vec::new(),
7814            },
7815        ];
7816
7817        assert_eq!(
7818            plan_push_commands(
7819                ObjectFormat::Sha1,
7820                &local_refs,
7821                &remote_refs,
7822                &[parse_refspec("refs/heads/main").expect("test operation should succeed")],
7823            )
7824            .expect("test operation should succeed"),
7825            vec![ReceivePackCommand {
7826                old_id: old.clone(),
7827                new_id: new.clone(),
7828                name: "refs/heads/main".into(),
7829            }]
7830        );
7831        assert_eq!(
7832            plan_push_commands(
7833                ObjectFormat::Sha1,
7834                &local_refs,
7835                &remote_refs,
7836                &[parse_refspec("refs/heads/new:refs/heads/new")
7837                    .expect("test operation should succeed")],
7838            )
7839            .expect("test operation should succeed"),
7840            vec![ReceivePackCommand {
7841                old_id: zero.clone(),
7842                new_id: new.clone(),
7843                name: "refs/heads/new".into(),
7844            }]
7845        );
7846        assert_eq!(
7847            plan_push_commands(
7848                ObjectFormat::Sha1,
7849                &local_refs,
7850                &remote_refs,
7851                &[parse_refspec(":refs/heads/old").expect("test operation should succeed")],
7852            )
7853            .expect("test operation should succeed"),
7854            vec![ReceivePackCommand {
7855                old_id: old.clone(),
7856                new_id: zero,
7857                name: "refs/heads/old".into(),
7858            }]
7859        );
7860        assert_eq!(
7861            plan_push_commands(
7862                ObjectFormat::Sha1,
7863                &local_refs,
7864                &remote_refs,
7865                &[parse_refspec(":").expect("test operation should succeed")],
7866            )
7867            .expect("test operation should succeed"),
7868            vec![ReceivePackCommand {
7869                old_id: old,
7870                new_id: new,
7871                name: "refs/heads/main".into(),
7872            }]
7873        );
7874    }
7875
7876    #[test]
7877    fn push_planner_builds_wildcard_commands_and_rejects_bad_refspecs() {
7878        let new = ObjectId::from_hex(
7879            ObjectFormat::Sha1,
7880            "2222222222222222222222222222222222222222",
7881        )
7882        .expect("test operation should succeed");
7883        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
7884        let local_refs = vec![PushSourceRef {
7885            oid: new.clone(),
7886            name: "refs/heads/topic".into(),
7887        }];
7888        let commands = plan_push_commands(
7889            ObjectFormat::Sha1,
7890            &local_refs,
7891            &[],
7892            &[parse_refspec("refs/heads/*:refs/heads/review/*")
7893                .expect("test operation should succeed")],
7894        )
7895        .expect("test operation should succeed");
7896        assert_eq!(
7897            commands,
7898            vec![ReceivePackCommand {
7899                old_id: zero,
7900                new_id: new,
7901                name: "refs/heads/review/topic".into(),
7902            }]
7903        );
7904        assert!(
7905            plan_push_commands(
7906                ObjectFormat::Sha1,
7907                &local_refs,
7908                &[],
7909                &[parse_refspec("^refs/heads/topic").expect("test operation should succeed")],
7910            )
7911            .is_err()
7912        );
7913        assert!(
7914            plan_push_commands(
7915                ObjectFormat::Sha1,
7916                &local_refs,
7917                &[],
7918                &[parse_refspec(":refs/heads/missing").expect("test operation should succeed")],
7919            )
7920            .is_err()
7921        );
7922    }
7923
7924    #[test]
7925    fn receive_pack_push_request_builder_negotiates_capabilities() {
7926        let old_id = ObjectId::from_hex(
7927            ObjectFormat::Sha1,
7928            "1111111111111111111111111111111111111111",
7929        )
7930        .expect("test operation should succeed");
7931        let new_id = ObjectId::from_hex(
7932            ObjectFormat::Sha1,
7933            "2222222222222222222222222222222222222222",
7934        )
7935        .expect("test operation should succeed");
7936        let features = ReceivePackFeatures {
7937            report_status_v2: true,
7938            atomic: true,
7939            ofs_delta: true,
7940            push_options: true,
7941            side_band_64k: true,
7942            quiet: true,
7943            object_format: Some(ObjectFormat::Sha1),
7944            ..ReceivePackFeatures::default()
7945        };
7946        let request = build_receive_pack_push_request(
7947            &features,
7948            vec![ReceivePackCommand {
7949                old_id,
7950                new_id,
7951                name: "refs/heads/main".into(),
7952            }],
7953            b"PACKdata".to_vec(),
7954            ReceivePackPushRequestOptions {
7955                report_status_v2: true,
7956                atomic: true,
7957                ofs_delta: true,
7958                side_band_64k: true,
7959                quiet: true,
7960                agent: Some("sley/0".into()),
7961                object_format: Some(ObjectFormat::Sha1),
7962                push_options: vec!["ci.skip".into()],
7963                ..ReceivePackPushRequestOptions::default()
7964            },
7965        )
7966        .expect("test operation should succeed");
7967        assert_eq!(
7968            request.commands.capabilities,
7969            vec![
7970                Capability {
7971                    name: "report-status-v2".into(),
7972                    value: None,
7973                },
7974                Capability {
7975                    name: "atomic".into(),
7976                    value: None,
7977                },
7978                Capability {
7979                    name: "ofs-delta".into(),
7980                    value: None,
7981                },
7982                Capability {
7983                    name: "side-band-64k".into(),
7984                    value: None,
7985                },
7986                Capability {
7987                    name: "quiet".into(),
7988                    value: None,
7989                },
7990                Capability {
7991                    name: "agent".into(),
7992                    value: Some("sley/0".into()),
7993                },
7994                Capability {
7995                    name: "object-format".into(),
7996                    value: Some("sha1".into()),
7997                },
7998                Capability {
7999                    name: "push-options".into(),
8000                    value: None,
8001                },
8002            ]
8003        );
8004        assert_eq!(request.push_options, Some(vec!["ci.skip".into()]));
8005        validate_receive_pack_push_request_features(&features, &request)
8006            .expect("test operation should succeed");
8007    }
8008
8009    #[test]
8010    fn receive_pack_push_request_builder_handles_delete_only_and_rejects_unadvertised() {
8011        let old_id = ObjectId::from_hex(
8012            ObjectFormat::Sha1,
8013            "1111111111111111111111111111111111111111",
8014        )
8015        .expect("test operation should succeed");
8016        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
8017        let features = ReceivePackFeatures {
8018            delete_refs: true,
8019            ..ReceivePackFeatures::default()
8020        };
8021        let request = build_receive_pack_push_request(
8022            &features,
8023            vec![ReceivePackCommand {
8024                old_id,
8025                new_id: zero,
8026                name: "refs/heads/old".into(),
8027            }],
8028            Vec::new(),
8029            ReceivePackPushRequestOptions::default(),
8030        )
8031        .expect("test operation should succeed");
8032        assert_eq!(
8033            request.commands.capabilities,
8034            vec![Capability {
8035                name: "delete-refs".into(),
8036                value: None,
8037            }]
8038        );
8039        assert!(request.packfile.is_empty());
8040
8041        assert!(
8042            build_receive_pack_push_request(
8043                &ReceivePackFeatures::default(),
8044                request.commands.commands.clone(),
8045                Vec::new(),
8046                ReceivePackPushRequestOptions::default(),
8047            )
8048            .is_err()
8049        );
8050        assert!(
8051            build_receive_pack_push_request(
8052                &features,
8053                request.commands.commands,
8054                b"PACK".to_vec(),
8055                ReceivePackPushRequestOptions::default(),
8056            )
8057            .is_err()
8058        );
8059        assert!(
8060            build_receive_pack_push_request(
8061                &features,
8062                Vec::new(),
8063                Vec::new(),
8064                ReceivePackPushRequestOptions {
8065                    push_options: vec!["ci.skip".into()],
8066                    ..ReceivePackPushRequestOptions::default()
8067                },
8068            )
8069            .is_err()
8070        );
8071    }
8072
8073    #[test]
8074    fn smart_http_helpers_build_paths_and_content_types() {
8075        let sha1 = ObjectId::from_hex(
8076            ObjectFormat::Sha1,
8077            "1111111111111111111111111111111111111111",
8078        )
8079        .expect("test operation should succeed");
8080        let sha256 = ObjectId::from_hex(
8081            ObjectFormat::Sha256,
8082            "2222222222222222222222222222222222222222222222222222222222222222",
8083        )
8084        .expect("test operation should succeed");
8085        assert_eq!(
8086            smart_http_info_refs_path("/repo.git/", GitService::UploadPack)
8087                .expect("test operation should succeed"),
8088            "/repo.git/info/refs?service=git-upload-pack"
8089        );
8090        assert_eq!(
8091            dumb_http_info_refs_path("/repo.git/").expect("test operation should succeed"),
8092            "/repo.git/info/refs"
8093        );
8094        assert_eq!(
8095            dumb_http_alternates_path("/repo.git").expect("test operation should succeed"),
8096            "/repo.git/objects/info/http-alternates"
8097        );
8098        assert_eq!(
8099            dumb_http_packs_path("/repo.git/").expect("test operation should succeed"),
8100            "/repo.git/objects/info/packs"
8101        );
8102        assert_eq!(
8103            dumb_http_loose_object_path("/repo.git/", &sha1)
8104                .expect("test operation should succeed"),
8105            "/repo.git/objects/11/11111111111111111111111111111111111111"
8106        );
8107        assert_eq!(
8108            dumb_http_loose_object_path("/repo.git/", &sha256)
8109                .expect("test operation should succeed"),
8110            "/repo.git/objects/22/22222222222222222222222222222222222222222222222222222222222222"
8111        );
8112        assert_eq!(
8113            dumb_http_pack_file_path("/repo.git/", &sha1).expect("test operation should succeed"),
8114            "/repo.git/objects/pack/pack-1111111111111111111111111111111111111111.pack"
8115        );
8116        assert_eq!(
8117            dumb_http_pack_index_path("/repo.git/", &sha1).expect("test operation should succeed"),
8118            "/repo.git/objects/pack/pack-1111111111111111111111111111111111111111.idx"
8119        );
8120        assert_eq!(
8121            smart_http_rpc_path("/repo.git", GitService::ReceivePack)
8122                .expect("test operation should succeed"),
8123            "/repo.git/git-receive-pack"
8124        );
8125        assert_eq!(
8126            smart_http_advertisement_content_type(GitService::UploadPack)
8127                .expect("test operation should succeed"),
8128            "application/x-git-upload-pack-advertisement"
8129        );
8130        assert_eq!(
8131            smart_http_rpc_request_content_type(GitService::UploadPack)
8132                .expect("test operation should succeed"),
8133            "application/x-git-upload-pack-request"
8134        );
8135        assert_eq!(
8136            smart_http_rpc_result_content_type(GitService::ReceivePack)
8137                .expect("test operation should succeed"),
8138            "application/x-git-receive-pack-result"
8139        );
8140        assert_eq!(
8141            parse_smart_http_advertisement_content_type(
8142                "Application/X-Git-Upload-Pack-Advertisement"
8143            )
8144            .expect("test operation should succeed"),
8145            GitService::UploadPack
8146        );
8147        assert_eq!(
8148            parse_smart_http_rpc_request_content_type("application/x-git-receive-pack-request")
8149                .expect("test operation should succeed"),
8150            GitService::ReceivePack
8151        );
8152        assert_eq!(
8153            parse_smart_http_rpc_result_content_type("application/x-git-upload-pack-result")
8154                .expect("test operation should succeed"),
8155            GitService::UploadPack
8156        );
8157    }
8158
8159    #[test]
8160    fn smart_http_helpers_reject_invalid_services_paths_and_content_types() {
8161        let oid = ObjectId::from_hex(
8162            ObjectFormat::Sha1,
8163            "1111111111111111111111111111111111111111",
8164        )
8165        .expect("test operation should succeed");
8166        assert!(smart_http_info_refs_path("repo.git", GitService::UploadPack).is_err());
8167        assert!(smart_http_rpc_path("/repo.git?x=1", GitService::UploadPack).is_err());
8168        assert!(dumb_http_info_refs_path("repo.git").is_err());
8169        assert!(dumb_http_alternates_path("/repo.git#fragment").is_err());
8170        assert!(dumb_http_packs_path("/repo.git?query").is_err());
8171        assert!(dumb_http_loose_object_path("repo.git", &oid).is_err());
8172        assert!(dumb_http_pack_file_path("/repo.git#fragment", &oid).is_err());
8173        assert!(dumb_http_pack_index_path("/repo.git?query", &oid).is_err());
8174        assert!(smart_http_info_refs_path("/repo.git", GitService::UploadArchive).is_err());
8175        assert!(smart_http_advertisement_content_type(GitService::UploadArchive).is_err());
8176        assert!(
8177            parse_smart_http_advertisement_content_type(
8178                "application/x-git-upload-archive-advertisement"
8179            )
8180            .is_err()
8181        );
8182        assert!(
8183            parse_smart_http_rpc_request_content_type("application/x-git-upload-pack-result")
8184                .is_err()
8185        );
8186        assert!(
8187            parse_smart_http_rpc_result_content_type(
8188                "application/x-git-receive-pack-result; charset=utf-8"
8189            )
8190            .is_err()
8191        );
8192    }
8193
8194    #[test]
8195    fn sideband_packets_parse_and_encode_channels() {
8196        let payloads = vec![
8197            b"\x01PACK bytes".to_vec(),
8198            b"\x02counting objects\n".to_vec(),
8199            b"\x03fatal error\n".to_vec(),
8200        ];
8201        let packets = parse_sideband_packets(&payloads).expect("test operation should succeed");
8202        assert_eq!(
8203            packets,
8204            vec![
8205                SideBandPacket {
8206                    channel: SideBandChannel::Data,
8207                    data: b"PACK bytes".to_vec(),
8208                },
8209                SideBandPacket {
8210                    channel: SideBandChannel::Progress,
8211                    data: b"counting objects\n".to_vec(),
8212                },
8213                SideBandPacket {
8214                    channel: SideBandChannel::Fatal,
8215                    data: b"fatal error\n".to_vec(),
8216                },
8217            ]
8218        );
8219        assert_eq!(
8220            encode_sideband_packets(&packets).expect("test operation should succeed"),
8221            payloads
8222        );
8223    }
8224
8225    #[test]
8226    fn sideband_stream_parses_encodes_and_demuxes_packets() {
8227        let frames = vec![
8228            PktLineFrame::Data(vec![1, b'P', b'A']),
8229            PktLineFrame::Data(vec![2, b'c', b'o', b'u', b'n', b't', b'\n']),
8230            PktLineFrame::Data(vec![1, b'C', b'K']),
8231            PktLineFrame::Flush,
8232        ];
8233        let packets = parse_sideband_stream(&frames).expect("test operation should succeed");
8234        assert_eq!(
8235            packets,
8236            vec![
8237                SideBandPacket {
8238                    channel: SideBandChannel::Data,
8239                    data: b"PA".to_vec(),
8240                },
8241                SideBandPacket {
8242                    channel: SideBandChannel::Progress,
8243                    data: b"count\n".to_vec(),
8244                },
8245                SideBandPacket {
8246                    channel: SideBandChannel::Data,
8247                    data: b"CK".to_vec(),
8248                },
8249            ]
8250        );
8251        assert_eq!(
8252            encode_sideband_stream(&packets).expect("test operation should succeed"),
8253            frames
8254        );
8255        assert_eq!(
8256            demux_sideband_stream(&frames).expect("test operation should succeed"),
8257            SideBandDemux {
8258                data: b"PACK".to_vec(),
8259                progress: vec![b"count\n".to_vec()],
8260            }
8261        );
8262    }
8263
8264    #[test]
8265    fn sideband_stream_reads_and_writes_until_flush() {
8266        let packets = vec![
8267            SideBandPacket {
8268                channel: SideBandChannel::Data,
8269                data: b"PACK".to_vec(),
8270            },
8271            SideBandPacket {
8272                channel: SideBandChannel::Progress,
8273                data: b"done\n".to_vec(),
8274            },
8275        ];
8276        let mut encoded = Vec::new();
8277        write_sideband_stream(&mut encoded, &packets).expect("test operation should succeed");
8278        encoded.extend_from_slice(b"tail");
8279
8280        let mut input = encoded.as_slice();
8281        assert_eq!(
8282            read_sideband_stream(&mut input).expect("test operation should succeed"),
8283            packets
8284        );
8285        assert_eq!(input, b"tail");
8286
8287        let mut input = encoded.as_slice();
8288        assert_eq!(
8289            read_and_demux_sideband_stream(&mut input).expect("test operation should succeed"),
8290            SideBandDemux {
8291                data: b"PACK".to_vec(),
8292                progress: vec![b"done\n".to_vec()],
8293            }
8294        );
8295        assert_eq!(input, b"tail");
8296    }
8297
8298    #[test]
8299    fn sideband_packets_demux_data_and_progress() {
8300        let payloads = vec![
8301            b"\x01PACK".to_vec(),
8302            b"\x02counting objects\n".to_vec(),
8303            b"\x01 bytes".to_vec(),
8304            b"\x02done\n".to_vec(),
8305        ];
8306        assert_eq!(
8307            parse_and_demux_sideband_packets(&payloads).expect("test operation should succeed"),
8308            SideBandDemux {
8309                data: b"PACK bytes".to_vec(),
8310                progress: vec![b"counting objects\n".to_vec(), b"done\n".to_vec()],
8311            }
8312        );
8313    }
8314
8315    #[test]
8316    fn sideband_packets_reject_bad_channels_and_oversize_payloads() {
8317        assert!(parse_sideband_packet(b"").is_err());
8318        assert!(parse_sideband_packet(b"\x04bad").is_err());
8319        assert!(
8320            parse_sideband_stream(&[PktLineFrame::Data(vec![1, b'P', b'A', b'C', b'K'])]).is_err()
8321        );
8322        assert!(parse_sideband_stream(&[PktLineFrame::Delimiter, PktLineFrame::Flush]).is_err());
8323        assert!(
8324            parse_sideband_stream(&[
8325                PktLineFrame::Data(vec![1, b'P', b'A']),
8326                PktLineFrame::Flush,
8327                PktLineFrame::Data(vec![1, b'C', b'K']),
8328            ])
8329            .is_err()
8330        );
8331        assert!(
8332            parse_sideband_stream(&[
8333                PktLineFrame::Data(vec![1, b'P', b'A']),
8334                PktLineFrame::Data(b"\x04bad".to_vec()),
8335                PktLineFrame::Flush,
8336            ])
8337            .is_err()
8338        );
8339        assert!(
8340            encode_sideband_packet(&SideBandPacket {
8341                channel: SideBandChannel::Data,
8342                data: vec![0; PKT_LINE_MAX_PAYLOAD_LEN],
8343            })
8344            .is_err()
8345        );
8346        assert!(
8347            demux_sideband_packets(&[SideBandPacket {
8348                channel: SideBandChannel::Fatal,
8349                data: b"remote died\n".to_vec(),
8350            }])
8351            .is_err()
8352        );
8353    }
8354
8355    #[test]
8356    fn upload_archive_request_parses_and_encodes_arguments() {
8357        let frames = vec![
8358            PktLineFrame::Data(b"argument --format=tar\n".to_vec()),
8359            PktLineFrame::Data(b"argument HEAD:dir with spaces\n".to_vec()),
8360            PktLineFrame::Flush,
8361        ];
8362        let request = parse_upload_archive_request(&frames).expect("test operation should succeed");
8363        assert_eq!(
8364            request,
8365            UploadArchiveRequest {
8366                arguments: vec!["--format=tar".into(), "HEAD:dir with spaces".into()],
8367            }
8368        );
8369        assert_eq!(
8370            encode_upload_archive_request(&request).expect("test operation should succeed"),
8371            frames
8372        );
8373    }
8374
8375    #[test]
8376    fn upload_archive_request_streams_round_trip() {
8377        let request = UploadArchiveRequest {
8378            arguments: vec!["--prefix=src/".into(), "main".into()],
8379        };
8380        let mut encoded = Vec::new();
8381        write_upload_archive_request(&mut encoded, &request)
8382            .expect("test operation should succeed");
8383        encoded.extend_from_slice(b"tail");
8384
8385        let mut input = encoded.as_slice();
8386        assert_eq!(
8387            read_upload_archive_request(&mut input).expect("test operation should succeed"),
8388            request
8389        );
8390        assert_eq!(input, b"tail");
8391    }
8392
8393    #[test]
8394    fn upload_archive_request_rejects_malformed_streams() {
8395        assert!(parse_upload_archive_request(&[PktLineFrame::Flush]).is_err());
8396        assert!(
8397            parse_upload_archive_request(&[
8398                PktLineFrame::Data(b"--format=tar\n".to_vec()),
8399                PktLineFrame::Flush,
8400            ])
8401            .is_err()
8402        );
8403        assert!(
8404            parse_upload_archive_request(&[
8405                PktLineFrame::Data(b"argument HEAD\n".to_vec()),
8406                PktLineFrame::Delimiter,
8407                PktLineFrame::Flush,
8408            ])
8409            .is_err()
8410        );
8411        assert!(
8412            encode_upload_archive_request(&UploadArchiveRequest {
8413                arguments: vec!["bad\narg".into()],
8414            })
8415            .is_err()
8416        );
8417    }
8418
8419    #[test]
8420    fn upload_archive_response_parses_ack_sideband_and_nack() {
8421        let ack_frames = vec![
8422            PktLineFrame::Data(b"ACK\n".to_vec()),
8423            PktLineFrame::Data(b"\x01tar bytes".to_vec()),
8424            PktLineFrame::Data(b"\x02progress\n".to_vec()),
8425            PktLineFrame::Flush,
8426        ];
8427        let response =
8428            parse_upload_archive_response(&ack_frames).expect("test operation should succeed");
8429        assert_eq!(
8430            response,
8431            UploadArchiveResponse::Ack {
8432                sideband: vec![
8433                    SideBandPacket {
8434                        channel: SideBandChannel::Data,
8435                        data: b"tar bytes".to_vec(),
8436                    },
8437                    SideBandPacket {
8438                        channel: SideBandChannel::Progress,
8439                        data: b"progress\n".to_vec(),
8440                    },
8441                ],
8442            }
8443        );
8444        assert_eq!(
8445            encode_upload_archive_response(&response).expect("test operation should succeed"),
8446            ack_frames
8447        );
8448        assert_eq!(
8449            demux_upload_archive_response(&response).expect("test operation should succeed"),
8450            SideBandDemux {
8451                data: b"tar bytes".to_vec(),
8452                progress: vec![b"progress\n".to_vec()],
8453            }
8454        );
8455
8456        let nack = UploadArchiveResponse::Nack {
8457            message: "unreachable tree".into(),
8458        };
8459        let nack_frames = vec![
8460            PktLineFrame::Data(b"NACK unreachable tree\n".to_vec()),
8461            PktLineFrame::Flush,
8462        ];
8463        assert_eq!(
8464            parse_upload_archive_response(&nack_frames).expect("test operation should succeed"),
8465            nack
8466        );
8467        assert_eq!(
8468            encode_upload_archive_response(&nack).expect("test operation should succeed"),
8469            nack_frames
8470        );
8471        assert!(demux_upload_archive_response(&nack).is_err());
8472    }
8473
8474    #[test]
8475    fn upload_archive_response_streams_round_trip() {
8476        let response = UploadArchiveResponse::Ack {
8477            sideband: vec![SideBandPacket {
8478                channel: SideBandChannel::Data,
8479                data: b"tar bytes".to_vec(),
8480            }],
8481        };
8482        let mut encoded = Vec::new();
8483        write_upload_archive_response(&mut encoded, &response)
8484            .expect("test operation should succeed");
8485        encoded.extend_from_slice(b"tail");
8486
8487        let mut input = encoded.as_slice();
8488        assert_eq!(
8489            read_upload_archive_response(&mut input).expect("test operation should succeed"),
8490            response
8491        );
8492        assert_eq!(input, b"tail");
8493    }
8494
8495    #[test]
8496    fn upload_archive_response_rejects_malformed_streams() {
8497        assert!(parse_upload_archive_response(&[]).is_err());
8498        assert!(
8499            parse_upload_archive_response(&[
8500                PktLineFrame::Data(b"ACK\n".to_vec()),
8501                PktLineFrame::Flush,
8502                PktLineFrame::Data(b"\x01tail".to_vec()),
8503            ])
8504            .is_err()
8505        );
8506        assert!(
8507            parse_upload_archive_response(&[
8508                PktLineFrame::Data(b"NACK\n".to_vec()),
8509                PktLineFrame::Flush,
8510            ])
8511            .is_err()
8512        );
8513        assert!(
8514            parse_upload_archive_response(&[
8515                PktLineFrame::Data(b"NACK denied\n".to_vec()),
8516                PktLineFrame::Data(b"\x02extra\n".to_vec()),
8517                PktLineFrame::Flush,
8518            ])
8519            .is_err()
8520        );
8521        assert!(
8522            encode_upload_archive_response(&UploadArchiveResponse::Nack {
8523                message: "bad\nmessage".into(),
8524            })
8525            .is_err()
8526        );
8527    }
8528
8529    #[test]
8530    fn capabilities_parse_and_encode_tokens() {
8531        let capabilities = parse_capabilities(
8532            b"multi_ack thin-pack agent=git/2.54.0 symref=HEAD:refs/heads/main\n",
8533        )
8534        .expect("test operation should succeed");
8535        assert_eq!(
8536            capabilities,
8537            vec![
8538                Capability {
8539                    name: "multi_ack".into(),
8540                    value: None,
8541                },
8542                Capability {
8543                    name: "thin-pack".into(),
8544                    value: None,
8545                },
8546                Capability {
8547                    name: "agent".into(),
8548                    value: Some("git/2.54.0".into()),
8549                },
8550                Capability {
8551                    name: "symref".into(),
8552                    value: Some("HEAD:refs/heads/main".into()),
8553                },
8554            ]
8555        );
8556        assert_eq!(
8557            encode_capabilities(&capabilities).expect("test operation should succeed"),
8558            b"multi_ack thin-pack agent=git/2.54.0 symref=HEAD:refs/heads/main"
8559        );
8560    }
8561
8562    #[test]
8563    fn capabilities_reject_empty_or_delimited_fields() {
8564        assert!(parse_capabilities(b"multi_ack  thin-pack").is_err());
8565        assert!(parse_capabilities(b"agent=").is_err());
8566        assert!(parse_capabilities(b"symref=HEAD:refs/heads/main\nbad").is_err());
8567        assert!(
8568            encode_capabilities(&[Capability {
8569                name: "bad name".into(),
8570                value: None,
8571            }])
8572            .is_err()
8573        );
8574    }
8575
8576    #[test]
8577    fn protocol_v2_object_format_uses_capability_or_defaults_to_sha1() {
8578        assert_eq!(
8579            protocol_v2_object_format(&[]).expect("test operation should succeed"),
8580            ObjectFormat::Sha1
8581        );
8582        assert_eq!(
8583            protocol_v2_object_format(&[Capability {
8584                name: "object-format".into(),
8585                value: Some("sha256".into()),
8586            }])
8587            .expect("test operation should succeed"),
8588            ObjectFormat::Sha256
8589        );
8590        assert!(
8591            protocol_v2_object_format(&[Capability {
8592                name: "object-format".into(),
8593                value: None,
8594            }])
8595            .is_err()
8596        );
8597        assert!(
8598            protocol_v2_object_format(&[
8599                Capability {
8600                    name: "object-format".into(),
8601                    value: Some("sha1".into()),
8602                },
8603                Capability {
8604                    name: "object-format".into(),
8605                    value: Some("sha256".into()),
8606                },
8607            ])
8608            .is_err()
8609        );
8610        assert!(
8611            protocol_v2_object_format(&[Capability {
8612                name: "object-format".into(),
8613                value: Some("unknown".into()),
8614            }])
8615            .is_err()
8616        );
8617    }
8618
8619    #[test]
8620    fn protocol_v2_command_request_capabilities_validate_against_handshake() {
8621        let handshake = TransportHandshake {
8622            protocol: ProtocolVersion::V2,
8623            capabilities: vec![
8624                Capability {
8625                    name: "fetch".into(),
8626                    value: Some("shallow filter".into()),
8627                },
8628                Capability {
8629                    name: "agent".into(),
8630                    value: Some("sley/0".into()),
8631                },
8632                Capability {
8633                    name: "object-format".into(),
8634                    value: Some("sha1".into()),
8635                },
8636            ],
8637        };
8638        validate_protocol_v2_command_request_capabilities(
8639            &handshake,
8640            &ProtocolV2CommandRequest {
8641                command: "fetch".into(),
8642                capabilities: vec![
8643                    Capability {
8644                        name: "agent".into(),
8645                        value: Some("client/1".into()),
8646                    },
8647                    Capability {
8648                        name: "object-format".into(),
8649                        value: Some("sha1".into()),
8650                    },
8651                ],
8652                arguments: Vec::new(),
8653            },
8654        )
8655        .expect("test operation should succeed");
8656        assert!(
8657            validate_protocol_v2_command_request_capabilities(
8658                &handshake,
8659                &ProtocolV2CommandRequest {
8660                    command: "ls-refs".into(),
8661                    capabilities: Vec::new(),
8662                    arguments: Vec::new(),
8663                },
8664            )
8665            .is_err()
8666        );
8667        assert!(
8668            validate_protocol_v2_command_request_capabilities(
8669                &handshake,
8670                &ProtocolV2CommandRequest {
8671                    command: "fetch".into(),
8672                    capabilities: vec![Capability {
8673                        name: "server-option".into(),
8674                        value: None,
8675                    }],
8676                    arguments: Vec::new(),
8677                },
8678            )
8679            .is_err()
8680        );
8681        assert!(
8682            validate_protocol_v2_command_request_capabilities(
8683                &handshake,
8684                &ProtocolV2CommandRequest {
8685                    command: "fetch".into(),
8686                    capabilities: vec![Capability {
8687                        name: "object-format".into(),
8688                        value: Some("sha256".into()),
8689                    }],
8690                    arguments: Vec::new(),
8691                },
8692            )
8693            .is_err()
8694        );
8695        assert!(
8696            validate_protocol_v2_command_request_capabilities(
8697                &handshake,
8698                &ProtocolV2CommandRequest {
8699                    command: "fetch".into(),
8700                    capabilities: vec![Capability {
8701                        name: "agent".into(),
8702                        value: None,
8703                    }],
8704                    arguments: Vec::new(),
8705                },
8706            )
8707            .is_err()
8708        );
8709    }
8710
8711    #[test]
8712    fn protocol_v2_command_options_parse_and_encode_known_capabilities() {
8713        let capabilities = vec![
8714            Capability {
8715                name: "agent".into(),
8716                value: Some("sley/0".into()),
8717            },
8718            Capability {
8719                name: "object-format".into(),
8720                value: Some("sha256".into()),
8721            },
8722            Capability {
8723                name: "server-option".into(),
8724                value: Some("trace=true".into()),
8725            },
8726            Capability {
8727                name: "server-option".into(),
8728                value: Some("region=west".into()),
8729            },
8730            Capability {
8731                name: "session-id".into(),
8732                value: Some("abc123".into()),
8733            },
8734        ];
8735        let options = parse_protocol_v2_command_options(&capabilities)
8736            .expect("test operation should succeed");
8737        assert_eq!(
8738            options,
8739            ProtocolV2CommandOptions {
8740                agent: Some("sley/0".into()),
8741                object_format: Some(ObjectFormat::Sha256),
8742                server_options: vec!["trace=true".into(), "region=west".into()],
8743                extra: vec![Capability {
8744                    name: "session-id".into(),
8745                    value: Some("abc123".into()),
8746                }],
8747            }
8748        );
8749        assert_eq!(
8750            encode_protocol_v2_command_options(&options).expect("test operation should succeed"),
8751            capabilities
8752        );
8753    }
8754
8755    #[test]
8756    fn protocol_v2_command_options_reject_malformed_known_capabilities() {
8757        assert!(
8758            parse_protocol_v2_command_options(&[
8759                Capability {
8760                    name: "agent".into(),
8761                    value: Some("sley/0".into()),
8762                },
8763                Capability {
8764                    name: "agent".into(),
8765                    value: Some("sley/1".into()),
8766                },
8767            ])
8768            .is_err()
8769        );
8770        assert!(
8771            parse_protocol_v2_command_options(&[Capability {
8772                name: "object-format".into(),
8773                value: Some("sha512".into()),
8774            }])
8775            .is_err()
8776        );
8777        assert!(
8778            parse_protocol_v2_command_options(&[Capability {
8779                name: "server-option".into(),
8780                value: None,
8781            }])
8782            .is_err()
8783        );
8784        assert!(
8785            encode_protocol_v2_command_options(&ProtocolV2CommandOptions {
8786                extra: vec![Capability {
8787                    name: "server-option".into(),
8788                    value: Some("trace=true".into()),
8789                }],
8790                ..ProtocolV2CommandOptions::default()
8791            })
8792            .is_err()
8793        );
8794    }
8795
8796    #[test]
8797    fn protocol_v2_ls_refs_features_parse_and_encode_advertisement() {
8798        let capabilities = vec![Capability {
8799            name: "ls-refs".into(),
8800            value: Some("unborn custom".into()),
8801        }];
8802        let features = parse_protocol_v2_ls_refs_features(&capabilities)
8803            .expect("test operation should succeed")
8804            .expect("test operation should succeed");
8805        assert_eq!(
8806            features,
8807            ProtocolV2LsRefsFeatures {
8808                unborn: true,
8809                unknown: vec!["custom".into()],
8810            }
8811        );
8812        assert_eq!(
8813            encode_protocol_v2_ls_refs_capability(&features)
8814                .expect("test operation should succeed"),
8815            capabilities[0]
8816        );
8817        assert_eq!(
8818            parse_protocol_v2_ls_refs_features(&[Capability {
8819                name: "ls-refs".into(),
8820                value: None,
8821            }])
8822            .expect("test operation should succeed")
8823            .expect("test operation should succeed"),
8824            ProtocolV2LsRefsFeatures::default()
8825        );
8826        assert!(
8827            parse_protocol_v2_ls_refs_features(&[Capability {
8828                name: "fetch".into(),
8829                value: Some("filter".into()),
8830            }])
8831            .expect("test operation should succeed")
8832            .is_none()
8833        );
8834    }
8835
8836    #[test]
8837    fn protocol_v2_ls_refs_features_reject_malformed_advertisements() {
8838        assert!(
8839            parse_protocol_v2_ls_refs_features(&[
8840                Capability {
8841                    name: "ls-refs".into(),
8842                    value: None,
8843                },
8844                Capability {
8845                    name: "ls-refs".into(),
8846                    value: None,
8847                },
8848            ])
8849            .is_err()
8850        );
8851        assert!(
8852            parse_protocol_v2_ls_refs_features(&[Capability {
8853                name: "ls-refs".into(),
8854                value: Some("unborn  custom".into()),
8855            }])
8856            .is_err()
8857        );
8858        assert!(
8859            encode_protocol_v2_ls_refs_capability(&ProtocolV2LsRefsFeatures {
8860                unknown: vec!["unborn".into()],
8861                ..ProtocolV2LsRefsFeatures::default()
8862            })
8863            .is_err()
8864        );
8865    }
8866
8867    #[test]
8868    fn protocol_v2_ls_refs_command_request_validates_unborn_feature() {
8869        let handshake = TransportHandshake {
8870            protocol: ProtocolVersion::V2,
8871            capabilities: vec![Capability {
8872                name: "ls-refs".into(),
8873                value: Some("unborn".into()),
8874            }],
8875        };
8876        let request = ProtocolV2CommandRequest {
8877            command: "ls-refs".into(),
8878            capabilities: Vec::new(),
8879            arguments: vec![b"unborn".to_vec(), b"ref-prefix HEAD".to_vec()],
8880        };
8881        let parsed = validate_protocol_v2_ls_refs_command_request(&handshake, &request)
8882            .expect("test operation should succeed");
8883        assert!(parsed.unborn);
8884        assert_eq!(parsed.ref_prefixes, vec!["HEAD"]);
8885
8886        let blocked = TransportHandshake {
8887            protocol: ProtocolVersion::V2,
8888            capabilities: vec![Capability {
8889                name: "ls-refs".into(),
8890                value: None,
8891            }],
8892        };
8893        assert!(validate_protocol_v2_ls_refs_command_request(&blocked, &request).is_err());
8894    }
8895
8896    #[test]
8897    fn protocol_v2_fetch_features_parse_and_encode_advertisement() {
8898        let capabilities = vec![Capability {
8899            name: "fetch".into(),
8900            value: Some(
8901                "shallow wait-for-done filter ref-in-want sideband-all packfile-uris custom".into(),
8902            ),
8903        }];
8904        let features = parse_protocol_v2_fetch_features(&capabilities)
8905            .expect("test operation should succeed")
8906            .expect("test operation should succeed");
8907        assert_eq!(
8908            features,
8909            ProtocolV2FetchFeatures {
8910                shallow: true,
8911                wait_for_done: true,
8912                filter: true,
8913                ref_in_want: true,
8914                sideband_all: true,
8915                packfile_uris: true,
8916                unknown: vec!["custom".into()],
8917            }
8918        );
8919        assert_eq!(
8920            encode_protocol_v2_fetch_capability(&features).expect("test operation should succeed"),
8921            capabilities[0]
8922        );
8923        assert_eq!(
8924            parse_protocol_v2_fetch_features(&[Capability {
8925                name: "fetch".into(),
8926                value: None,
8927            }])
8928            .expect("test operation should succeed")
8929            .expect("test operation should succeed"),
8930            ProtocolV2FetchFeatures::default()
8931        );
8932        assert!(
8933            parse_protocol_v2_fetch_features(&[])
8934                .expect("test operation should succeed")
8935                .is_none()
8936        );
8937    }
8938
8939    #[test]
8940    fn protocol_v2_fetch_features_reject_malformed_advertisements() {
8941        assert!(
8942            parse_protocol_v2_fetch_features(&[
8943                Capability {
8944                    name: "fetch".into(),
8945                    value: None,
8946                },
8947                Capability {
8948                    name: "fetch".into(),
8949                    value: None,
8950                },
8951            ])
8952            .is_err()
8953        );
8954        assert!(
8955            parse_protocol_v2_fetch_features(&[Capability {
8956                name: "fetch".into(),
8957                value: Some("filter  shallow".into()),
8958            }])
8959            .is_err()
8960        );
8961        assert!(
8962            encode_protocol_v2_fetch_capability(&ProtocolV2FetchFeatures {
8963                unknown: vec!["filter".into()],
8964                ..ProtocolV2FetchFeatures::default()
8965            })
8966            .is_err()
8967        );
8968    }
8969
8970    #[test]
8971    fn protocol_v2_fetch_request_features_validate_feature_gated_arguments() {
8972        let features = ProtocolV2FetchFeatures {
8973            shallow: true,
8974            wait_for_done: true,
8975            filter: true,
8976            ref_in_want: true,
8977            sideband_all: true,
8978            packfile_uris: true,
8979            unknown: Vec::new(),
8980        };
8981        validate_protocol_v2_fetch_request_features(
8982            &features,
8983            &ProtocolV2FetchRequest {
8984                want_refs: vec!["refs/heads/main".into()],
8985                shallow: vec![
8986                    ObjectId::from_hex(
8987                        ObjectFormat::Sha1,
8988                        "1111111111111111111111111111111111111111",
8989                    )
8990                    .expect("test operation should succeed"),
8991                ],
8992                deepen: Some(1),
8993                filter: Some("blob:none".into()),
8994                packfile_uris: Some("https".into()),
8995                sideband_all: true,
8996                wait_for_done: true,
8997                ..ProtocolV2FetchRequest::default()
8998            },
8999        )
9000        .expect("test operation should succeed");
9001
9002        let request = ProtocolV2FetchRequest {
9003            want_refs: vec!["refs/heads/main".into()],
9004            filter: Some("blob:none".into()),
9005            sideband_all: true,
9006            ..ProtocolV2FetchRequest::default()
9007        };
9008        assert!(
9009            validate_protocol_v2_fetch_request_features(
9010                &ProtocolV2FetchFeatures::default(),
9011                &request,
9012            )
9013            .is_err()
9014        );
9015        assert!(
9016            validate_protocol_v2_fetch_request_features(
9017                &ProtocolV2FetchFeatures {
9018                    ref_in_want: true,
9019                    filter: true,
9020                    ..ProtocolV2FetchFeatures::default()
9021                },
9022                &request,
9023            )
9024            .is_err()
9025        );
9026    }
9027
9028    #[test]
9029    fn protocol_v2_fetch_command_request_validates_against_handshake_features() {
9030        let handshake = TransportHandshake {
9031            protocol: ProtocolVersion::V2,
9032            capabilities: vec![
9033                Capability {
9034                    name: "fetch".into(),
9035                    value: Some("filter ref-in-want".into()),
9036                },
9037                Capability {
9038                    name: "agent".into(),
9039                    value: Some("sley/0".into()),
9040                },
9041            ],
9042        };
9043        let request = ProtocolV2CommandRequest {
9044            command: "fetch".into(),
9045            capabilities: vec![Capability {
9046                name: "agent".into(),
9047                value: Some("client/1".into()),
9048            }],
9049            arguments: vec![
9050                b"want-ref refs/heads/main".to_vec(),
9051                b"filter blob:none".to_vec(),
9052            ],
9053        };
9054        let fetch =
9055            validate_protocol_v2_fetch_command_request(&handshake, ObjectFormat::Sha1, &request)
9056                .expect("test operation should succeed");
9057        assert_eq!(fetch.want_refs, vec!["refs/heads/main"]);
9058        assert_eq!(fetch.filter.as_deref(), Some("blob:none"));
9059
9060        let mut bad = request.clone();
9061        bad.arguments.push(b"sideband-all".to_vec());
9062        assert!(
9063            validate_protocol_v2_fetch_command_request(&handshake, ObjectFormat::Sha1, &bad)
9064                .is_err()
9065        );
9066    }
9067
9068    #[test]
9069    fn protocol_v2_object_info_request_parses_encodes_and_validates() {
9070        let oid = ObjectId::from_hex(
9071            ObjectFormat::Sha1,
9072            "1111111111111111111111111111111111111111",
9073        )
9074        .expect("test operation should succeed");
9075        let request = ProtocolV2CommandRequest {
9076            command: "object-info".into(),
9077            capabilities: Vec::new(),
9078            arguments: vec![
9079                b"size".to_vec(),
9080                b"oid 1111111111111111111111111111111111111111".to_vec(),
9081            ],
9082        };
9083        let parsed =
9084            ProtocolV2ObjectInfoRequest::from_command_request(ObjectFormat::Sha1, &request)
9085                .expect("test operation should succeed");
9086        assert_eq!(
9087            parsed,
9088            ProtocolV2ObjectInfoRequest {
9089                size: true,
9090                oids: vec![oid],
9091            }
9092        );
9093        assert_eq!(
9094            parsed
9095                .to_command_request()
9096                .expect("test operation should succeed"),
9097            request
9098        );
9099
9100        let handshake = TransportHandshake {
9101            protocol: ProtocolVersion::V2,
9102            capabilities: vec![Capability {
9103                name: "object-info".into(),
9104                value: None,
9105            }],
9106        };
9107        assert_eq!(
9108            validate_protocol_v2_object_info_command_request(
9109                &handshake,
9110                ObjectFormat::Sha1,
9111                &request,
9112            )
9113            .expect("test operation should succeed"),
9114            parsed
9115        );
9116    }
9117
9118    #[test]
9119    fn protocol_v2_object_info_request_streams_round_trip() {
9120        let request = ProtocolV2ObjectInfoRequest {
9121            size: true,
9122            oids: vec![
9123                ObjectId::from_hex(
9124                    ObjectFormat::Sha1,
9125                    "1111111111111111111111111111111111111111",
9126                )
9127                .expect("test operation should succeed"),
9128            ],
9129        };
9130        let mut encoded = Vec::new();
9131        write_protocol_v2_object_info_request(&mut encoded, &request)
9132            .expect("test operation should succeed");
9133        encoded.extend_from_slice(b"tail");
9134
9135        let mut input = encoded.as_slice();
9136        assert_eq!(
9137            read_protocol_v2_object_info_request(ObjectFormat::Sha1, &mut input)
9138                .expect("test operation should succeed"),
9139            request
9140        );
9141        assert_eq!(input, b"tail");
9142    }
9143
9144    #[test]
9145    fn protocol_v2_object_info_request_rejects_malformed_arguments() {
9146        assert!(
9147            ProtocolV2ObjectInfoRequest::from_command_request(
9148                ObjectFormat::Sha1,
9149                &ProtocolV2CommandRequest {
9150                    command: "object-info".into(),
9151                    capabilities: Vec::new(),
9152                    arguments: vec![b"oid 1111111111111111111111111111111111111111".to_vec()],
9153                },
9154            )
9155            .is_err()
9156        );
9157        assert!(
9158            ProtocolV2ObjectInfoRequest::from_command_request(
9159                ObjectFormat::Sha1,
9160                &ProtocolV2CommandRequest {
9161                    command: "object-info".into(),
9162                    capabilities: Vec::new(),
9163                    arguments: vec![b"size".to_vec(), b"size".to_vec()],
9164                },
9165            )
9166            .is_err()
9167        );
9168        assert!(
9169            ProtocolV2ObjectInfoRequest::from_command_request(
9170                ObjectFormat::Sha1,
9171                &ProtocolV2CommandRequest {
9172                    command: "object-info".into(),
9173                    capabilities: Vec::new(),
9174                    arguments: vec![b"size".to_vec()],
9175                },
9176            )
9177            .is_err()
9178        );
9179        assert!(
9180            ProtocolV2ObjectInfoRequest::from_command_request(
9181                ObjectFormat::Sha1,
9182                &ProtocolV2CommandRequest {
9183                    command: "object-info".into(),
9184                    capabilities: Vec::new(),
9185                    arguments: vec![b"size".to_vec(), b"oid not-an-oid".to_vec()],
9186                },
9187            )
9188            .is_err()
9189        );
9190        assert!(
9191            validate_protocol_v2_object_info_command_request(
9192                &TransportHandshake {
9193                    protocol: ProtocolVersion::V2,
9194                    capabilities: Vec::new(),
9195                },
9196                ObjectFormat::Sha1,
9197                &ProtocolV2CommandRequest {
9198                    command: "object-info".into(),
9199                    capabilities: Vec::new(),
9200                    arguments: vec![
9201                        b"size".to_vec(),
9202                        b"oid 1111111111111111111111111111111111111111".to_vec(),
9203                    ],
9204                },
9205            )
9206            .is_err()
9207        );
9208    }
9209
9210    #[test]
9211    fn protocol_v2_command_request_classifies_known_and_unknown_commands() {
9212        let handshake = TransportHandshake {
9213            protocol: ProtocolVersion::V2,
9214            capabilities: vec![
9215                Capability {
9216                    name: "ls-refs".into(),
9217                    value: Some("unborn".into()),
9218                },
9219                Capability {
9220                    name: "fetch".into(),
9221                    value: Some("filter ref-in-want".into()),
9222                },
9223                Capability {
9224                    name: "object-info".into(),
9225                    value: None,
9226                },
9227                Capability {
9228                    name: "server-option".into(),
9229                    value: None,
9230                },
9231                Capability {
9232                    name: "server-info".into(),
9233                    value: Some("custom".into()),
9234                },
9235            ],
9236        };
9237        assert_eq!(
9238            classify_protocol_v2_command_request(
9239                &handshake,
9240                ObjectFormat::Sha1,
9241                &ProtocolV2CommandRequest {
9242                    command: "ls-refs".into(),
9243                    capabilities: Vec::new(),
9244                    arguments: vec![b"unborn".to_vec()],
9245                },
9246            )
9247            .expect("test operation should succeed"),
9248            ProtocolV2Command::LsRefs(ProtocolV2LsRefsRequest {
9249                unborn: true,
9250                ..ProtocolV2LsRefsRequest::default()
9251            })
9252        );
9253        assert_eq!(
9254            classify_protocol_v2_command_request(
9255                &handshake,
9256                ObjectFormat::Sha1,
9257                &ProtocolV2CommandRequest {
9258                    command: "fetch".into(),
9259                    capabilities: Vec::new(),
9260                    arguments: vec![
9261                        b"want-ref refs/heads/main".to_vec(),
9262                        b"filter blob:none".to_vec(),
9263                    ],
9264                },
9265            )
9266            .expect("test operation should succeed"),
9267            ProtocolV2Command::Fetch(ProtocolV2FetchRequest {
9268                want_refs: vec!["refs/heads/main".into()],
9269                filter: Some("blob:none".into()),
9270                ..ProtocolV2FetchRequest::default()
9271            })
9272        );
9273        assert_eq!(
9274            classify_protocol_v2_command_request(
9275                &handshake,
9276                ObjectFormat::Sha1,
9277                &ProtocolV2CommandRequest {
9278                    command: "object-info".into(),
9279                    capabilities: Vec::new(),
9280                    arguments: vec![
9281                        b"size".to_vec(),
9282                        b"oid 1111111111111111111111111111111111111111".to_vec(),
9283                    ],
9284                },
9285            )
9286            .expect("test operation should succeed"),
9287            ProtocolV2Command::ObjectInfo(ProtocolV2ObjectInfoRequest {
9288                size: true,
9289                oids: vec![
9290                    ObjectId::from_hex(
9291                        ObjectFormat::Sha1,
9292                        "1111111111111111111111111111111111111111",
9293                    )
9294                    .expect("test operation should succeed")
9295                ],
9296            })
9297        );
9298
9299        let unknown = ProtocolV2CommandRequest {
9300            command: "server-info".into(),
9301            capabilities: vec![Capability {
9302                name: "server-option".into(),
9303                value: Some("trace=true".into()),
9304            }],
9305            arguments: Vec::new(),
9306        };
9307        assert_eq!(
9308            classify_protocol_v2_command_request(&handshake, ObjectFormat::Sha1, &unknown)
9309                .expect("test operation should succeed"),
9310            ProtocolV2Command::Unknown(unknown)
9311        );
9312        assert!(
9313            classify_protocol_v2_command_request(
9314                &handshake,
9315                ObjectFormat::Sha1,
9316                &ProtocolV2CommandRequest {
9317                    command: "not-advertised".into(),
9318                    capabilities: Vec::new(),
9319                    arguments: Vec::new(),
9320                },
9321            )
9322            .is_err()
9323        );
9324    }
9325
9326    #[test]
9327    fn protocol_v2_session_request_classifies_streamed_command_and_done() {
9328        let handshake = TransportHandshake {
9329            protocol: ProtocolVersion::V2,
9330            capabilities: vec![
9331                Capability {
9332                    name: "ls-refs".into(),
9333                    value: Some("unborn".into()),
9334                },
9335                Capability {
9336                    name: "fetch".into(),
9337                    value: Some("filter ref-in-want".into()),
9338                },
9339            ],
9340        };
9341        let command = ProtocolV2Request::Command(ProtocolV2CommandRequest {
9342            command: "ls-refs".into(),
9343            capabilities: Vec::new(),
9344            arguments: vec![b"unborn".to_vec()],
9345        });
9346        assert_eq!(
9347            classify_protocol_v2_request(&handshake, ObjectFormat::Sha1, &command)
9348                .expect("test operation should succeed"),
9349            ProtocolV2SessionRequest::Command(ProtocolV2Command::LsRefs(ProtocolV2LsRefsRequest {
9350                unborn: true,
9351                ..ProtocolV2LsRefsRequest::default()
9352            }))
9353        );
9354        assert_eq!(
9355            classify_protocol_v2_request(&handshake, ObjectFormat::Sha1, &ProtocolV2Request::Done)
9356                .expect("test operation should succeed"),
9357            ProtocolV2SessionRequest::Done
9358        );
9359
9360        let mut encoded = Vec::new();
9361        write_protocol_v2_request(&mut encoded, &command).expect("test operation should succeed");
9362        write_protocol_v2_request(&mut encoded, &ProtocolV2Request::Done)
9363            .expect("test operation should succeed");
9364        encoded.extend_from_slice(b"tail");
9365
9366        let mut input = encoded.as_slice();
9367        assert_eq!(
9368            read_protocol_v2_session_request(&handshake, ObjectFormat::Sha1, &mut input)
9369                .expect("test operation should succeed"),
9370            ProtocolV2SessionRequest::Command(ProtocolV2Command::LsRefs(ProtocolV2LsRefsRequest {
9371                unborn: true,
9372                ..ProtocolV2LsRefsRequest::default()
9373            }))
9374        );
9375        assert_eq!(
9376            read_protocol_v2_session_request(&handshake, ObjectFormat::Sha1, &mut input)
9377                .expect("test operation should succeed"),
9378            ProtocolV2SessionRequest::Done
9379        );
9380        assert_eq!(input, b"tail");
9381    }
9382
9383    #[test]
9384    fn advertised_ref_parses_first_v0_capability_line() {
9385        let payload =
9386            b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 HEAD\0multi_ack symref=HEAD:refs/heads/main\n";
9387        let advertisement = parse_ref_advertisement(ObjectFormat::Sha1, payload)
9388            .expect("test operation should succeed");
9389        assert_eq!(
9390            advertisement.oid,
9391            ObjectId::from_hex(
9392                ObjectFormat::Sha1,
9393                "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"
9394            )
9395            .expect("test operation should succeed")
9396        );
9397        assert_eq!(advertisement.name, "HEAD");
9398        assert_eq!(
9399            advertisement.capabilities,
9400            vec![
9401                Capability {
9402                    name: "multi_ack".into(),
9403                    value: None,
9404                },
9405                Capability {
9406                    name: "symref".into(),
9407                    value: Some("HEAD:refs/heads/main".into()),
9408                },
9409            ]
9410        );
9411    }
9412
9413    #[test]
9414    fn advertised_ref_parses_lines_without_capabilities() {
9415        let advertisement = parse_ref_advertisement(
9416            ObjectFormat::Sha1,
9417            b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n",
9418        )
9419        .expect("test operation should succeed");
9420        assert_eq!(advertisement.name, "refs/heads/main");
9421        assert!(advertisement.capabilities.is_empty());
9422    }
9423
9424    #[test]
9425    fn advertised_ref_rejects_malformed_payloads() {
9426        assert!(
9427            parse_ref_advertisement(ObjectFormat::Sha1, b"not-an-oid refs/heads/main\n").is_err()
9428        );
9429        assert!(
9430            parse_ref_advertisement(
9431                ObjectFormat::Sha1,
9432                b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n"
9433            )
9434            .is_err()
9435        );
9436    }
9437
9438    #[test]
9439    fn advertised_refs_parse_and_encode_stream() {
9440        let main = ObjectId::from_hex(
9441            ObjectFormat::Sha1,
9442            "1111111111111111111111111111111111111111",
9443        )
9444        .expect("test operation should succeed");
9445        let feature = ObjectId::from_hex(
9446            ObjectFormat::Sha1,
9447            "2222222222222222222222222222222222222222",
9448        )
9449        .expect("test operation should succeed");
9450        let frames = vec![
9451            PktLineFrame::Data(
9452                b"1111111111111111111111111111111111111111 HEAD\0multi_ack thin-pack agent=git/2.54.0\n"
9453                    .to_vec(),
9454            ),
9455            PktLineFrame::Data(
9456                b"2222222222222222222222222222222222222222 refs/heads/feature\n".to_vec(),
9457            ),
9458            PktLineFrame::Flush,
9459        ];
9460        let advertisements = parse_ref_advertisements(ObjectFormat::Sha1, &frames)
9461            .expect("test operation should succeed");
9462        assert_eq!(
9463            advertisements,
9464            vec![
9465                RefAdvertisement {
9466                    oid: main,
9467                    name: "HEAD".into(),
9468                    capabilities: vec![
9469                        Capability {
9470                            name: "multi_ack".into(),
9471                            value: None,
9472                        },
9473                        Capability {
9474                            name: "thin-pack".into(),
9475                            value: None,
9476                        },
9477                        Capability {
9478                            name: "agent".into(),
9479                            value: Some("git/2.54.0".into()),
9480                        },
9481                    ],
9482                },
9483                RefAdvertisement {
9484                    oid: feature,
9485                    name: "refs/heads/feature".into(),
9486                    capabilities: Vec::new(),
9487                },
9488            ]
9489        );
9490        assert_eq!(
9491            encode_ref_advertisements(&advertisements).expect("test operation should succeed"),
9492            frames
9493        );
9494        assert_eq!(
9495            parse_ref_advertisements(ObjectFormat::Sha1, &[PktLineFrame::Flush])
9496                .expect("test operation should succeed"),
9497            Vec::<RefAdvertisement>::new()
9498        );
9499    }
9500
9501    #[test]
9502    fn advertised_ref_set_parses_v1_version_refs_and_shallow() {
9503        let main = ObjectId::from_hex(
9504            ObjectFormat::Sha1,
9505            "1111111111111111111111111111111111111111",
9506        )
9507        .expect("test operation should succeed");
9508        let feature = ObjectId::from_hex(
9509            ObjectFormat::Sha1,
9510            "2222222222222222222222222222222222222222",
9511        )
9512        .expect("test operation should succeed");
9513        let shallow = ObjectId::from_hex(
9514            ObjectFormat::Sha1,
9515            "3333333333333333333333333333333333333333",
9516        )
9517        .expect("test operation should succeed");
9518        let frames = vec![
9519            PktLineFrame::Data(b"version 1\n".to_vec()),
9520            PktLineFrame::Data(
9521                b"1111111111111111111111111111111111111111 HEAD\0multi_ack symref=HEAD:refs/heads/main\n"
9522                    .to_vec(),
9523            ),
9524            PktLineFrame::Data(
9525                b"2222222222222222222222222222222222222222 refs/heads/feature\n".to_vec(),
9526            ),
9527            PktLineFrame::Data(b"shallow 3333333333333333333333333333333333333333\n".to_vec()),
9528            PktLineFrame::Flush,
9529        ];
9530
9531        let set = parse_ref_advertisement_set(ObjectFormat::Sha1, &frames)
9532            .expect("test operation should succeed");
9533        assert_eq!(set.protocol, ProtocolVersion::V1);
9534        assert_eq!(set.shallow, vec![shallow]);
9535        assert_eq!(
9536            set.refs,
9537            vec![
9538                RefAdvertisement {
9539                    oid: main,
9540                    name: "HEAD".into(),
9541                    capabilities: vec![
9542                        Capability {
9543                            name: "multi_ack".into(),
9544                            value: None,
9545                        },
9546                        Capability {
9547                            name: "symref".into(),
9548                            value: Some("HEAD:refs/heads/main".into()),
9549                        },
9550                    ],
9551                },
9552                RefAdvertisement {
9553                    oid: feature,
9554                    name: "refs/heads/feature".into(),
9555                    capabilities: Vec::new(),
9556                },
9557            ]
9558        );
9559        assert_eq!(
9560            parse_ref_advertisements(ObjectFormat::Sha1, &frames)
9561                .expect("test operation should succeed"),
9562            set.refs
9563        );
9564        assert_eq!(
9565            encode_ref_advertisement_set(&set).expect("test operation should succeed"),
9566            frames
9567        );
9568    }
9569
9570    #[test]
9571    fn advertised_refs_streams_round_trip() {
9572        let advertisements = vec![RefAdvertisement {
9573            oid: ObjectId::from_hex(
9574                ObjectFormat::Sha1,
9575                "1111111111111111111111111111111111111111",
9576            )
9577            .expect("test operation should succeed"),
9578            name: "HEAD".into(),
9579            capabilities: vec![Capability {
9580                name: "symref".into(),
9581                value: Some("HEAD:refs/heads/main".into()),
9582            }],
9583        }];
9584        let mut encoded = Vec::new();
9585        write_ref_advertisements(&mut encoded, &advertisements)
9586            .expect("test operation should succeed");
9587        encoded.extend_from_slice(b"tail");
9588
9589        let mut input = encoded.as_slice();
9590        assert_eq!(
9591            read_ref_advertisements(ObjectFormat::Sha1, &mut input)
9592                .expect("test operation should succeed"),
9593            advertisements
9594        );
9595        assert_eq!(input, b"tail");
9596    }
9597
9598    #[test]
9599    fn advertised_ref_set_streams_round_trip() {
9600        let set = RefAdvertisementSet {
9601            protocol: ProtocolVersion::V1,
9602            refs: vec![RefAdvertisement {
9603                oid: ObjectId::from_hex(
9604                    ObjectFormat::Sha1,
9605                    "1111111111111111111111111111111111111111",
9606                )
9607                .expect("test operation should succeed"),
9608                name: "HEAD".into(),
9609                capabilities: vec![Capability {
9610                    name: "symref".into(),
9611                    value: Some("HEAD:refs/heads/main".into()),
9612                }],
9613            }],
9614            shallow: vec![
9615                ObjectId::from_hex(
9616                    ObjectFormat::Sha1,
9617                    "2222222222222222222222222222222222222222",
9618                )
9619                .expect("test operation should succeed"),
9620            ],
9621        };
9622        let mut encoded = Vec::new();
9623        write_ref_advertisement_set(&mut encoded, &set).expect("test operation should succeed");
9624        encoded.extend_from_slice(b"tail");
9625
9626        let mut input = encoded.as_slice();
9627        assert_eq!(
9628            read_ref_advertisement_set(ObjectFormat::Sha1, &mut input)
9629                .expect("test operation should succeed"),
9630            set
9631        );
9632        assert_eq!(input, b"tail");
9633    }
9634
9635    #[test]
9636    fn advertised_refs_reject_malformed_streams() {
9637        assert!(
9638            parse_ref_advertisements(
9639                ObjectFormat::Sha1,
9640                &[PktLineFrame::Data(
9641                    b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),
9642                )],
9643            )
9644            .is_err()
9645        );
9646        assert!(
9647            parse_ref_advertisements(
9648                ObjectFormat::Sha1,
9649                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
9650            )
9651            .is_err()
9652        );
9653        assert!(parse_ref_advertisements(
9654            ObjectFormat::Sha1,
9655            &[
9656                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
9657                PktLineFrame::Data(
9658                    b"2222222222222222222222222222222222222222 refs/heads/main\0thin-pack\n"
9659                        .to_vec(),
9660                ),
9661                PktLineFrame::Flush,
9662            ],
9663        )
9664        .is_err());
9665        assert!(parse_ref_advertisement_set(
9666            ObjectFormat::Sha1,
9667            &[
9668                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
9669                PktLineFrame::Data(b"version 1\n".to_vec()),
9670                PktLineFrame::Flush,
9671            ],
9672        )
9673        .is_err());
9674        assert!(
9675            parse_ref_advertisement_set(
9676                ObjectFormat::Sha1,
9677                &[
9678                    PktLineFrame::Data(b"version 2\n".to_vec()),
9679                    PktLineFrame::Flush,
9680                ],
9681            )
9682            .is_err()
9683        );
9684        assert!(
9685            parse_ref_advertisement_set(
9686                ObjectFormat::Sha1,
9687                &[
9688                    PktLineFrame::Data(
9689                        b"shallow 1111111111111111111111111111111111111111\n".to_vec()
9690                    ),
9691                    PktLineFrame::Flush,
9692                ],
9693            )
9694            .is_err()
9695        );
9696        assert!(parse_ref_advertisement_set(
9697            ObjectFormat::Sha1,
9698            &[
9699                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
9700                PktLineFrame::Data(b"shallow not-an-oid\n".to_vec()),
9701                PktLineFrame::Flush,
9702            ],
9703        )
9704        .is_err());
9705        assert!(parse_ref_advertisement_set(
9706            ObjectFormat::Sha1,
9707            &[
9708                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
9709                PktLineFrame::Data(b"shallow 2222222222222222222222222222222222222222\n".to_vec()),
9710                PktLineFrame::Data(
9711                    b"3333333333333333333333333333333333333333 refs/heads/main\n".to_vec(),
9712                ),
9713                PktLineFrame::Flush,
9714            ],
9715        )
9716        .is_err());
9717        assert!(
9718            encode_ref_advertisements(&[
9719                RefAdvertisement {
9720                    oid: ObjectId::from_hex(
9721                        ObjectFormat::Sha1,
9722                        "1111111111111111111111111111111111111111",
9723                    )
9724                    .expect("test operation should succeed"),
9725                    name: "HEAD".into(),
9726                    capabilities: Vec::new(),
9727                },
9728                RefAdvertisement {
9729                    oid: ObjectId::from_hex(
9730                        ObjectFormat::Sha1,
9731                        "2222222222222222222222222222222222222222",
9732                    )
9733                    .expect("test operation should succeed"),
9734                    name: "refs/heads/main".into(),
9735                    capabilities: vec![Capability {
9736                        name: "thin-pack".into(),
9737                        value: None,
9738                    }],
9739                },
9740            ])
9741            .is_err()
9742        );
9743        assert!(
9744            encode_ref_advertisement(&RefAdvertisement {
9745                oid: ObjectId::from_hex(
9746                    ObjectFormat::Sha1,
9747                    "1111111111111111111111111111111111111111",
9748                )
9749                .expect("test operation should succeed"),
9750                name: "bad ref".into(),
9751                capabilities: Vec::new(),
9752            })
9753            .is_err()
9754        );
9755        assert!(
9756            encode_ref_advertisement_set(&RefAdvertisementSet {
9757                protocol: ProtocolVersion::V2,
9758                refs: Vec::new(),
9759                shallow: Vec::new(),
9760            })
9761            .is_err()
9762        );
9763        assert!(
9764            encode_ref_advertisement_set(&RefAdvertisementSet {
9765                protocol: ProtocolVersion::V0,
9766                refs: Vec::new(),
9767                shallow: vec![
9768                    ObjectId::from_hex(
9769                        ObjectFormat::Sha1,
9770                        "1111111111111111111111111111111111111111",
9771                    )
9772                    .expect("test operation should succeed")
9773                ],
9774            })
9775            .is_err()
9776        );
9777    }
9778
9779    #[test]
9780    fn dumb_http_info_refs_parse_and_encode_records() {
9781        let main = ObjectId::from_hex(
9782            ObjectFormat::Sha1,
9783            "1111111111111111111111111111111111111111",
9784        )
9785        .expect("test operation should succeed");
9786        let tag = ObjectId::from_hex(
9787            ObjectFormat::Sha1,
9788            "2222222222222222222222222222222222222222",
9789        )
9790        .expect("test operation should succeed");
9791        let peeled = ObjectId::from_hex(
9792            ObjectFormat::Sha1,
9793            "3333333333333333333333333333333333333333",
9794        )
9795        .expect("test operation should succeed");
9796        let input = b"1111111111111111111111111111111111111111\trefs/heads/main\n2222222222222222222222222222222222222222\trefs/tags/v1.0\n3333333333333333333333333333333333333333\trefs/tags/v1.0^{}\n";
9797
9798        let records = parse_dumb_http_info_refs(ObjectFormat::Sha1, input)
9799            .expect("test operation should succeed");
9800        assert_eq!(
9801            records,
9802            vec![
9803                DumbHttpRefRecord {
9804                    oid: main,
9805                    name: "refs/heads/main".into(),
9806                    peeled: false,
9807                },
9808                DumbHttpRefRecord {
9809                    oid: tag,
9810                    name: "refs/tags/v1.0".into(),
9811                    peeled: false,
9812                },
9813                DumbHttpRefRecord {
9814                    oid: peeled,
9815                    name: "refs/tags/v1.0".into(),
9816                    peeled: true,
9817                },
9818            ]
9819        );
9820        assert_eq!(
9821            encode_dumb_http_info_refs(&records).expect("test operation should succeed"),
9822            input
9823        );
9824        assert_eq!(
9825            parse_dumb_http_info_refs(ObjectFormat::Sha1, b"")
9826                .expect("test operation should succeed"),
9827            Vec::<DumbHttpRefRecord>::new()
9828        );
9829    }
9830
9831    #[test]
9832    fn dumb_http_info_refs_streams_round_trip() {
9833        let records = vec![DumbHttpRefRecord {
9834            oid: ObjectId::from_hex(
9835                ObjectFormat::Sha1,
9836                "1111111111111111111111111111111111111111",
9837            )
9838            .expect("test operation should succeed"),
9839            name: "refs/heads/main".into(),
9840            peeled: false,
9841        }];
9842        let mut encoded = Vec::new();
9843        write_dumb_http_info_refs(&mut encoded, &records).expect("test operation should succeed");
9844        let mut input = encoded.as_slice();
9845        assert_eq!(
9846            read_dumb_http_info_refs(ObjectFormat::Sha1, &mut input)
9847                .expect("test operation should succeed"),
9848            records
9849        );
9850        assert!(input.is_empty());
9851    }
9852
9853    #[test]
9854    fn dumb_http_info_refs_reject_malformed_records() {
9855        assert!(
9856            parse_dumb_http_info_refs(
9857                ObjectFormat::Sha1,
9858                b"1111111111111111111111111111111111111111 refs/heads/main\n",
9859            )
9860            .is_err()
9861        );
9862        assert!(
9863            parse_dumb_http_info_refs(
9864                ObjectFormat::Sha1,
9865                b"1111111111111111111111111111111111111111\trefs/heads/main",
9866            )
9867            .is_err()
9868        );
9869        assert!(
9870            parse_dumb_http_info_refs(ObjectFormat::Sha1, b"not-an-oid\trefs/heads/main\n")
9871                .is_err()
9872        );
9873        assert!(
9874            parse_dumb_http_info_refs(
9875                ObjectFormat::Sha1,
9876                b"1111111111111111111111111111111111111111\tbad ref\n",
9877            )
9878            .is_err()
9879        );
9880        assert!(
9881            encode_dumb_http_info_refs(&[DumbHttpRefRecord {
9882                oid: ObjectId::from_hex(
9883                    ObjectFormat::Sha1,
9884                    "1111111111111111111111111111111111111111",
9885                )
9886                .expect("test operation should succeed"),
9887                name: "refs/tags/v1.0^{}".into(),
9888                peeled: false,
9889            }])
9890            .is_err()
9891        );
9892    }
9893
9894    #[test]
9895    fn dumb_http_alternates_parse_and_encode_locations() {
9896        let input = b"https://example.com/base.git/objects/\n../other.git/objects/\n";
9897        let alternates = parse_dumb_http_alternates(input).expect("test operation should succeed");
9898        assert_eq!(
9899            alternates,
9900            vec![
9901                "https://example.com/base.git/objects/".to_string(),
9902                "../other.git/objects/".to_string(),
9903            ]
9904        );
9905        assert_eq!(
9906            encode_dumb_http_alternates(&alternates).expect("test operation should succeed"),
9907            input
9908        );
9909        assert_eq!(
9910            parse_dumb_http_alternates(b"").expect("test operation should succeed"),
9911            Vec::<String>::new()
9912        );
9913    }
9914
9915    #[test]
9916    fn dumb_http_alternates_streams_round_trip() {
9917        let alternates = vec!["https://example.com/base.git/objects/".to_string()];
9918        let mut encoded = Vec::new();
9919        write_dumb_http_alternates(&mut encoded, &alternates)
9920            .expect("test operation should succeed");
9921        let mut input = encoded.as_slice();
9922        assert_eq!(
9923            read_dumb_http_alternates(&mut input).expect("test operation should succeed"),
9924            alternates
9925        );
9926        assert!(input.is_empty());
9927    }
9928
9929    #[test]
9930    fn dumb_http_alternates_reject_malformed_lines() {
9931        assert!(parse_dumb_http_alternates(b"https://example.com/base.git/objects/").is_err());
9932        assert!(parse_dumb_http_alternates(b"\n").is_err());
9933        assert!(parse_dumb_http_alternates(b"https://example.com/base.git/objects/\r\n").is_err());
9934        assert!(encode_dumb_http_alternates(&["bad\nalternate".to_string()]).is_err());
9935    }
9936
9937    #[test]
9938    fn dumb_http_packs_parse_and_encode_pack_records() {
9939        let first = ObjectId::from_hex(
9940            ObjectFormat::Sha1,
9941            "1111111111111111111111111111111111111111",
9942        )
9943        .expect("test operation should succeed");
9944        let second = ObjectId::from_hex(
9945            ObjectFormat::Sha1,
9946            "2222222222222222222222222222222222222222",
9947        )
9948        .expect("test operation should succeed");
9949        let input = b"P pack-1111111111111111111111111111111111111111.pack\nP pack-2222222222222222222222222222222222222222.pack\n";
9950        let records = parse_dumb_http_packs(ObjectFormat::Sha1, input)
9951            .expect("test operation should succeed");
9952        assert_eq!(
9953            records,
9954            vec![
9955                DumbHttpPackRecord { hash: first },
9956                DumbHttpPackRecord { hash: second },
9957            ]
9958        );
9959        assert_eq!(
9960            encode_dumb_http_packs(&records).expect("test operation should succeed"),
9961            input
9962        );
9963        assert_eq!(
9964            parse_dumb_http_packs(ObjectFormat::Sha1, b"").expect("test operation should succeed"),
9965            Vec::<DumbHttpPackRecord>::new()
9966        );
9967    }
9968
9969    #[test]
9970    fn dumb_http_packs_streams_round_trip() {
9971        let records = vec![DumbHttpPackRecord {
9972            hash: ObjectId::from_hex(
9973                ObjectFormat::Sha1,
9974                "1111111111111111111111111111111111111111",
9975            )
9976            .expect("test operation should succeed"),
9977        }];
9978        let mut encoded = Vec::new();
9979        write_dumb_http_packs(&mut encoded, &records).expect("test operation should succeed");
9980        let mut input = encoded.as_slice();
9981        assert_eq!(
9982            read_dumb_http_packs(ObjectFormat::Sha1, &mut input)
9983                .expect("test operation should succeed"),
9984            records
9985        );
9986        assert!(input.is_empty());
9987    }
9988
9989    #[test]
9990    fn dumb_http_packs_reject_malformed_records() {
9991        assert!(
9992            parse_dumb_http_packs(
9993                ObjectFormat::Sha1,
9994                b"P pack-1111111111111111111111111111111111111111.pack",
9995            )
9996            .is_err()
9997        );
9998        assert!(
9999            parse_dumb_http_packs(
10000                ObjectFormat::Sha1,
10001                b"pack-1111111111111111111111111111111111111111.pack\n",
10002            )
10003            .is_err()
10004        );
10005        assert!(parse_dumb_http_packs(ObjectFormat::Sha1, b"P pack-not-a-hash.pack\n",).is_err());
10006        assert!(
10007            parse_dumb_http_packs(
10008                ObjectFormat::Sha1,
10009                b"P pack-1111111111111111111111111111111111111111.idx\n",
10010            )
10011            .is_err()
10012        );
10013    }
10014
10015    #[test]
10016    fn upload_pack_features_parse_encode_and_validate_request() {
10017        let capabilities = vec![
10018            Capability {
10019                name: "multi_ack".into(),
10020                value: None,
10021            },
10022            Capability {
10023                name: "multi_ack_detailed".into(),
10024                value: None,
10025            },
10026            Capability {
10027                name: "no-done".into(),
10028                value: None,
10029            },
10030            Capability {
10031                name: "thin-pack".into(),
10032                value: None,
10033            },
10034            Capability {
10035                name: "side-band-64k".into(),
10036                value: None,
10037            },
10038            Capability {
10039                name: "ofs-delta".into(),
10040                value: None,
10041            },
10042            Capability {
10043                name: "shallow".into(),
10044                value: None,
10045            },
10046            Capability {
10047                name: "deepen-since".into(),
10048                value: None,
10049            },
10050            Capability {
10051                name: "deepen-not".into(),
10052                value: None,
10053            },
10054            Capability {
10055                name: "include-tag".into(),
10056                value: None,
10057            },
10058            Capability {
10059                name: "no-progress".into(),
10060                value: None,
10061            },
10062            Capability {
10063                name: "filter".into(),
10064                value: None,
10065            },
10066            Capability {
10067                name: "agent".into(),
10068                value: Some("git/2.54.0".into()),
10069            },
10070            Capability {
10071                name: "object-format".into(),
10072                value: Some("sha256".into()),
10073            },
10074            Capability {
10075                name: "symref".into(),
10076                value: Some("HEAD:refs/heads/main".into()),
10077            },
10078            Capability {
10079                name: "custom".into(),
10080                value: Some("value".into()),
10081            },
10082        ];
10083        let features =
10084            parse_upload_pack_features(&capabilities).expect("test operation should succeed");
10085        assert_eq!(
10086            features,
10087            UploadPackFeatures {
10088                multi_ack: true,
10089                multi_ack_detailed: true,
10090                no_done: true,
10091                thin_pack: true,
10092                side_band_64k: true,
10093                ofs_delta: true,
10094                shallow: true,
10095                deepen_since: true,
10096                deepen_not: true,
10097                include_tag: true,
10098                no_progress: true,
10099                filter: true,
10100                agent: Some("git/2.54.0".into()),
10101                object_format: Some(ObjectFormat::Sha256),
10102                symrefs: vec!["HEAD:refs/heads/main".into()],
10103                unknown: vec![Capability {
10104                    name: "custom".into(),
10105                    value: Some("value".into()),
10106                }],
10107                ..UploadPackFeatures::default()
10108            }
10109        );
10110        assert_eq!(
10111            encode_upload_pack_features(&features).expect("test operation should succeed"),
10112            capabilities
10113        );
10114
10115        let request = UploadPackRequest {
10116            wants: vec![
10117                ObjectId::from_hex(
10118                    ObjectFormat::Sha1,
10119                    "1111111111111111111111111111111111111111",
10120                )
10121                .expect("test operation should succeed"),
10122            ],
10123            capabilities: vec![
10124                Capability {
10125                    name: "multi_ack_detailed".into(),
10126                    value: None,
10127                },
10128                Capability {
10129                    name: "thin-pack".into(),
10130                    value: None,
10131                },
10132                Capability {
10133                    name: "side-band-64k".into(),
10134                    value: None,
10135                },
10136                Capability {
10137                    name: "ofs-delta".into(),
10138                    value: None,
10139                },
10140                Capability {
10141                    name: "include-tag".into(),
10142                    value: None,
10143                },
10144                Capability {
10145                    name: "agent".into(),
10146                    value: Some("sley".into()),
10147                },
10148            ],
10149            shallow: vec![
10150                ObjectId::from_hex(
10151                    ObjectFormat::Sha1,
10152                    "2222222222222222222222222222222222222222",
10153                )
10154                .expect("test operation should succeed"),
10155            ],
10156            deepen: Some(5),
10157            deepen_since: Some(1_710_000_000),
10158            deepen_not: vec!["refs/tags/base".into()],
10159            filter: Some("blob:none".into()),
10160        };
10161        validate_upload_pack_request_features(&features, &request)
10162            .expect("test operation should succeed");
10163    }
10164
10165    #[test]
10166    fn upload_pack_features_reject_invalid_requests() {
10167        let want = ObjectId::from_hex(
10168            ObjectFormat::Sha1,
10169            "1111111111111111111111111111111111111111",
10170        )
10171        .expect("test operation should succeed");
10172        let features = UploadPackFeatures {
10173            thin_pack: true,
10174            side_band: true,
10175            ..UploadPackFeatures::default()
10176        };
10177
10178        assert!(
10179            validate_upload_pack_request_features(
10180                &features,
10181                &UploadPackRequest {
10182                    wants: vec![want],
10183                    capabilities: vec![Capability {
10184                        name: "ofs-delta".into(),
10185                        value: None,
10186                    }],
10187                    ..UploadPackRequest::default()
10188                },
10189            )
10190            .is_err()
10191        );
10192        assert!(
10193            validate_upload_pack_request_features(
10194                &features,
10195                &UploadPackRequest {
10196                    wants: vec![want],
10197                    shallow: vec![want],
10198                    ..UploadPackRequest::default()
10199                },
10200            )
10201            .is_err()
10202        );
10203        assert!(
10204            validate_upload_pack_request_features(
10205                &features,
10206                &UploadPackRequest {
10207                    wants: vec![want],
10208                    filter: Some("blob:none".into()),
10209                    ..UploadPackRequest::default()
10210                },
10211            )
10212            .is_err()
10213        );
10214        assert!(
10215            validate_upload_pack_request_features(
10216                &UploadPackFeatures {
10217                    side_band: true,
10218                    side_band_64k: true,
10219                    ..UploadPackFeatures::default()
10220                },
10221                &UploadPackRequest {
10222                    wants: vec![want],
10223                    capabilities: vec![
10224                        Capability {
10225                            name: "side-band".into(),
10226                            value: None,
10227                        },
10228                        Capability {
10229                            name: "side-band-64k".into(),
10230                            value: None,
10231                        },
10232                    ],
10233                    ..UploadPackRequest::default()
10234                },
10235            )
10236            .is_err()
10237        );
10238
10239        assert!(
10240            parse_upload_pack_features(&[
10241                Capability {
10242                    name: "thin-pack".into(),
10243                    value: None,
10244                },
10245                Capability {
10246                    name: "thin-pack".into(),
10247                    value: None,
10248                },
10249            ])
10250            .is_err()
10251        );
10252        assert!(
10253            encode_upload_pack_features(&UploadPackFeatures {
10254                unknown: vec![Capability {
10255                    name: "filter".into(),
10256                    value: None,
10257                }],
10258                ..UploadPackFeatures::default()
10259            })
10260            .is_err()
10261        );
10262    }
10263
10264    #[test]
10265    fn upload_pack_raw_response_builder_filters_unknown_haves_and_builds_pack() {
10266        let want = ObjectId::from_hex(
10267            ObjectFormat::Sha1,
10268            "1111111111111111111111111111111111111111",
10269        )
10270        .expect("test operation should succeed");
10271        let known_have = ObjectId::from_hex(
10272            ObjectFormat::Sha1,
10273            "2222222222222222222222222222222222222222",
10274        )
10275        .expect("test operation should succeed");
10276        let unknown_have = ObjectId::from_hex(
10277            ObjectFormat::Sha1,
10278            "3333333333333333333333333333333333333333",
10279        )
10280        .expect("test operation should succeed");
10281        let existing = std::collections::HashSet::from([want, known_have]);
10282
10283        let response = build_upload_pack_raw_packfile_response(
10284            &UploadPackFeatures::default(),
10285            UploadPackRequest {
10286                wants: vec![want],
10287                ..UploadPackRequest::default()
10288            },
10289            [known_have, unknown_have],
10290            |oid| Ok(existing.contains(oid)),
10291            |wants, haves| {
10292                assert_eq!(wants, vec![want]);
10293                assert_eq!(haves, vec![known_have]);
10294                Ok(Some(b"PACKmock".to_vec()))
10295            },
10296        )
10297        .expect("test operation should succeed");
10298
10299        assert_eq!(
10300            response.acknowledgments,
10301            vec![UploadPackAcknowledgment::Nak]
10302        );
10303        assert_eq!(response.packfile, b"PACKmock");
10304    }
10305
10306    #[test]
10307    fn upload_pack_raw_response_builder_rejects_missing_want_and_empty_pack() {
10308        let want = ObjectId::from_hex(
10309            ObjectFormat::Sha1,
10310            "1111111111111111111111111111111111111111",
10311        )
10312        .expect("test operation should succeed");
10313
10314        assert!(
10315            build_upload_pack_raw_packfile_response(
10316                &UploadPackFeatures::default(),
10317                UploadPackRequest {
10318                    wants: vec![want],
10319                    ..UploadPackRequest::default()
10320                },
10321                Vec::<ObjectId>::new(),
10322                |_| Ok(false),
10323                |_, _| Ok(Some(b"PACKmock".to_vec())),
10324            )
10325            .is_err()
10326        );
10327
10328        assert!(
10329            build_upload_pack_raw_packfile_response(
10330                &UploadPackFeatures::default(),
10331                UploadPackRequest {
10332                    wants: vec![want],
10333                    ..UploadPackRequest::default()
10334                },
10335                Vec::<ObjectId>::new(),
10336                |_| Ok(true),
10337                |_, _| Ok(None),
10338            )
10339            .is_err()
10340        );
10341    }
10342
10343    #[test]
10344    fn upload_pack_request_parses_and_encodes_initial_fetch_request() {
10345        let want = ObjectId::from_hex(
10346            ObjectFormat::Sha1,
10347            "1111111111111111111111111111111111111111",
10348        )
10349        .expect("test operation should succeed");
10350        let second_want = ObjectId::from_hex(
10351            ObjectFormat::Sha1,
10352            "2222222222222222222222222222222222222222",
10353        )
10354        .expect("test operation should succeed");
10355        let shallow = ObjectId::from_hex(
10356            ObjectFormat::Sha1,
10357            "3333333333333333333333333333333333333333",
10358        )
10359        .expect("test operation should succeed");
10360        let frames = vec![
10361            PktLineFrame::Data(
10362                b"want 1111111111111111111111111111111111111111 multi_ack thin-pack agent=git/2.54.0\n"
10363                    .to_vec(),
10364            ),
10365            PktLineFrame::Data(b"want 2222222222222222222222222222222222222222\n".to_vec()),
10366            PktLineFrame::Data(b"shallow 3333333333333333333333333333333333333333\n".to_vec()),
10367            PktLineFrame::Data(b"deepen-since 1710000000\n".to_vec()),
10368            PktLineFrame::Data(b"deepen-not refs/tags/base\n".to_vec()),
10369            PktLineFrame::Data(b"filter blob:none\n".to_vec()),
10370            PktLineFrame::Flush,
10371        ];
10372        let request = parse_upload_pack_request(ObjectFormat::Sha1, &frames)
10373            .expect("test operation should succeed")
10374            .expect("test operation should succeed");
10375        assert_eq!(
10376            request,
10377            UploadPackRequest {
10378                wants: vec![want, second_want],
10379                capabilities: vec![
10380                    Capability {
10381                        name: "multi_ack".into(),
10382                        value: None,
10383                    },
10384                    Capability {
10385                        name: "thin-pack".into(),
10386                        value: None,
10387                    },
10388                    Capability {
10389                        name: "agent".into(),
10390                        value: Some("git/2.54.0".into()),
10391                    },
10392                ],
10393                shallow: vec![shallow],
10394                deepen: None,
10395                deepen_since: Some(1_710_000_000),
10396                deepen_not: vec!["refs/tags/base".into()],
10397                filter: Some("blob:none".into()),
10398            }
10399        );
10400        assert_eq!(
10401            encode_upload_pack_request(Some(&request)).expect("test operation should succeed"),
10402            frames
10403        );
10404        assert_eq!(
10405            parse_upload_pack_request(ObjectFormat::Sha1, &[PktLineFrame::Flush])
10406                .expect("test operation should succeed"),
10407            None
10408        );
10409        assert_eq!(
10410            encode_upload_pack_request(None).expect("test operation should succeed"),
10411            vec![PktLineFrame::Flush]
10412        );
10413    }
10414
10415    #[test]
10416    fn upload_pack_request_streams_round_trip() {
10417        let request = UploadPackRequest {
10418            wants: vec![
10419                ObjectId::from_hex(
10420                    ObjectFormat::Sha1,
10421                    "1111111111111111111111111111111111111111",
10422                )
10423                .expect("test operation should succeed"),
10424            ],
10425            capabilities: vec![Capability {
10426                name: "ofs-delta".into(),
10427                value: None,
10428            }],
10429            deepen: Some(10),
10430            ..UploadPackRequest::default()
10431        };
10432        let mut encoded = Vec::new();
10433        write_upload_pack_request(&mut encoded, Some(&request))
10434            .expect("test operation should succeed");
10435        encoded.extend_from_slice(b"tail");
10436
10437        let mut input = encoded.as_slice();
10438        assert_eq!(
10439            read_upload_pack_request(ObjectFormat::Sha1, &mut input)
10440                .expect("test operation should succeed"),
10441            Some(request)
10442        );
10443        assert_eq!(input, b"tail");
10444    }
10445
10446    #[test]
10447    fn upload_pack_request_rejects_malformed_requests() {
10448        assert!(
10449            parse_upload_pack_request(
10450                ObjectFormat::Sha1,
10451                &[PktLineFrame::Data(
10452                    b"want 1111111111111111111111111111111111111111\n".to_vec(),
10453                )],
10454            )
10455            .is_err()
10456        );
10457        assert!(
10458            parse_upload_pack_request(
10459                ObjectFormat::Sha1,
10460                &[
10461                    PktLineFrame::Data(
10462                        b"shallow 1111111111111111111111111111111111111111\n".to_vec(),
10463                    ),
10464                    PktLineFrame::Flush,
10465                ],
10466            )
10467            .is_err()
10468        );
10469        assert!(
10470            parse_upload_pack_request(
10471                ObjectFormat::Sha1,
10472                &[
10473                    PktLineFrame::Data(
10474                        b"want 1111111111111111111111111111111111111111 thin-pack\n".to_vec(),
10475                    ),
10476                    PktLineFrame::Data(
10477                        b"want 2222222222222222222222222222222222222222 ofs-delta\n".to_vec(),
10478                    ),
10479                    PktLineFrame::Flush,
10480                ],
10481            )
10482            .is_err()
10483        );
10484        assert!(parse_upload_pack_request(
10485            ObjectFormat::Sha1,
10486            &[
10487                PktLineFrame::Data(b"want 1111111111111111111111111111111111111111\n".to_vec(),),
10488                PktLineFrame::Data(b"deepen 1\n".to_vec()),
10489                PktLineFrame::Data(b"want 2222222222222222222222222222222222222222\n".to_vec()),
10490                PktLineFrame::Flush,
10491            ],
10492        )
10493        .is_err());
10494        assert!(parse_upload_pack_request(
10495            ObjectFormat::Sha1,
10496            &[
10497                PktLineFrame::Data(b"want 1111111111111111111111111111111111111111\n".to_vec(),),
10498                PktLineFrame::Data(b"filter blob:none\n".to_vec()),
10499                PktLineFrame::Data(b"filter tree:0\n".to_vec()),
10500                PktLineFrame::Flush,
10501            ],
10502        )
10503        .is_err());
10504        assert!(encode_upload_pack_request(Some(&UploadPackRequest::default())).is_err());
10505        assert!(
10506            encode_upload_pack_request(Some(&UploadPackRequest {
10507                wants: vec![
10508                    ObjectId::from_hex(
10509                        ObjectFormat::Sha1,
10510                        "1111111111111111111111111111111111111111",
10511                    )
10512                    .expect("test operation should succeed")
10513                ],
10514                deepen: Some(0),
10515                ..UploadPackRequest::default()
10516            }))
10517            .is_err()
10518        );
10519    }
10520
10521    #[test]
10522    fn upload_pack_shallow_update_parses_and_encodes_records() {
10523        let shallow = ObjectId::from_hex(
10524            ObjectFormat::Sha1,
10525            "1111111111111111111111111111111111111111",
10526        )
10527        .expect("test operation should succeed");
10528        let unshallow = ObjectId::from_hex(
10529            ObjectFormat::Sha1,
10530            "2222222222222222222222222222222222222222",
10531        )
10532        .expect("test operation should succeed");
10533        let frames = vec![
10534            PktLineFrame::Data(b"shallow 1111111111111111111111111111111111111111\n".to_vec()),
10535            PktLineFrame::Data(b"unshallow 2222222222222222222222222222222222222222\n".to_vec()),
10536            PktLineFrame::Flush,
10537        ];
10538        let entries = parse_upload_pack_shallow_update(ObjectFormat::Sha1, &frames)
10539            .expect("test operation should succeed");
10540        assert_eq!(
10541            entries,
10542            vec![
10543                ProtocolV2FetchShallowInfo::Shallow(shallow),
10544                ProtocolV2FetchShallowInfo::Unshallow(unshallow),
10545            ]
10546        );
10547        assert_eq!(
10548            encode_upload_pack_shallow_update(&entries).expect("test operation should succeed"),
10549            frames
10550        );
10551        assert_eq!(
10552            parse_upload_pack_shallow_update(ObjectFormat::Sha1, &[PktLineFrame::Flush])
10553                .expect("test operation should succeed"),
10554            Vec::<ProtocolV2FetchShallowInfo>::new()
10555        );
10556    }
10557
10558    #[test]
10559    fn upload_pack_shallow_update_streams_round_trip() {
10560        let entries = vec![ProtocolV2FetchShallowInfo::Shallow(
10561            ObjectId::from_hex(
10562                ObjectFormat::Sha1,
10563                "1111111111111111111111111111111111111111",
10564            )
10565            .expect("test operation should succeed"),
10566        )];
10567        let mut encoded = Vec::new();
10568        write_upload_pack_shallow_update(&mut encoded, &entries)
10569            .expect("test operation should succeed");
10570        encoded.extend_from_slice(b"tail");
10571
10572        let mut input = encoded.as_slice();
10573        assert_eq!(
10574            read_upload_pack_shallow_update(ObjectFormat::Sha1, &mut input)
10575                .expect("test operation should succeed"),
10576            entries
10577        );
10578        assert_eq!(input, b"tail");
10579    }
10580
10581    #[test]
10582    fn upload_pack_shallow_update_rejects_malformed_records() {
10583        assert!(
10584            parse_upload_pack_shallow_update(
10585                ObjectFormat::Sha1,
10586                &[PktLineFrame::Data(
10587                    b"shallow 1111111111111111111111111111111111111111\n".to_vec(),
10588                )],
10589            )
10590            .is_err()
10591        );
10592        assert!(
10593            parse_upload_pack_shallow_update(
10594                ObjectFormat::Sha1,
10595                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
10596            )
10597            .is_err()
10598        );
10599        assert!(
10600            parse_upload_pack_shallow_update(
10601                ObjectFormat::Sha1,
10602                &[
10603                    PktLineFrame::Data(
10604                        b"shallow 1111111111111111111111111111111111111111\n".to_vec(),
10605                    ),
10606                    PktLineFrame::Flush,
10607                    PktLineFrame::Data(
10608                        b"unshallow 2222222222222222222222222222222222222222\n".to_vec(),
10609                    ),
10610                ],
10611            )
10612            .is_err()
10613        );
10614        assert!(
10615            parse_upload_pack_shallow_update(
10616                ObjectFormat::Sha1,
10617                &[
10618                    PktLineFrame::Data(
10619                        b"unsupported 1111111111111111111111111111111111111111\n".to_vec(),
10620                    ),
10621                    PktLineFrame::Flush,
10622                ],
10623            )
10624            .is_err()
10625        );
10626    }
10627
10628    #[test]
10629    fn upload_pack_negotiation_request_parses_flush_and_done_rounds() {
10630        let have = ObjectId::from_hex(
10631            ObjectFormat::Sha1,
10632            "1111111111111111111111111111111111111111",
10633        )
10634        .expect("test operation should succeed");
10635        let second_have = ObjectId::from_hex(
10636            ObjectFormat::Sha1,
10637            "2222222222222222222222222222222222222222",
10638        )
10639        .expect("test operation should succeed");
10640        let flush_round = vec![
10641            PktLineFrame::Data(b"have 1111111111111111111111111111111111111111\n".to_vec()),
10642            PktLineFrame::Data(b"have 2222222222222222222222222222222222222222\n".to_vec()),
10643            PktLineFrame::Flush,
10644        ];
10645        let request = parse_upload_pack_negotiation_request(ObjectFormat::Sha1, &flush_round)
10646            .expect("test operation should succeed");
10647        assert_eq!(
10648            request,
10649            UploadPackNegotiationRequest {
10650                haves: vec![have, second_have],
10651                done: false,
10652            }
10653        );
10654        assert_eq!(
10655            encode_upload_pack_negotiation_request(&request)
10656                .expect("test operation should succeed"),
10657            flush_round
10658        );
10659
10660        let done_round = vec![
10661            PktLineFrame::Data(b"have 1111111111111111111111111111111111111111\n".to_vec()),
10662            PktLineFrame::Data(b"done\n".to_vec()),
10663        ];
10664        let request = parse_upload_pack_negotiation_request(ObjectFormat::Sha1, &done_round)
10665            .expect("test operation should succeed");
10666        assert_eq!(
10667            request,
10668            UploadPackNegotiationRequest {
10669                haves: vec![have],
10670                done: true,
10671            }
10672        );
10673        assert_eq!(
10674            encode_upload_pack_negotiation_request(&request)
10675                .expect("test operation should succeed"),
10676            done_round
10677        );
10678    }
10679
10680    #[test]
10681    fn upload_pack_negotiation_request_streams_round_trip() {
10682        let first = UploadPackNegotiationRequest {
10683            haves: vec![
10684                ObjectId::from_hex(
10685                    ObjectFormat::Sha1,
10686                    "1111111111111111111111111111111111111111",
10687                )
10688                .expect("test operation should succeed"),
10689            ],
10690            done: false,
10691        };
10692        let second = UploadPackNegotiationRequest {
10693            haves: Vec::new(),
10694            done: true,
10695        };
10696        let mut encoded = Vec::new();
10697        write_upload_pack_negotiation_request(&mut encoded, &first)
10698            .expect("test operation should succeed");
10699        write_upload_pack_negotiation_request(&mut encoded, &second)
10700            .expect("test operation should succeed");
10701        encoded.extend_from_slice(b"tail");
10702
10703        let mut input = encoded.as_slice();
10704        assert_eq!(
10705            read_upload_pack_negotiation_request(ObjectFormat::Sha1, &mut input)
10706                .expect("test operation should succeed"),
10707            first
10708        );
10709        assert_eq!(
10710            read_upload_pack_negotiation_request(ObjectFormat::Sha1, &mut input)
10711                .expect("test operation should succeed"),
10712            second
10713        );
10714        assert_eq!(input, b"tail");
10715    }
10716
10717    #[test]
10718    fn upload_pack_negotiation_request_rejects_malformed_rounds() {
10719        assert!(
10720            parse_upload_pack_negotiation_request(
10721                ObjectFormat::Sha1,
10722                &[PktLineFrame::Data(
10723                    b"have 1111111111111111111111111111111111111111\n".to_vec(),
10724                )],
10725            )
10726            .is_err()
10727        );
10728        assert!(
10729            parse_upload_pack_negotiation_request(
10730                ObjectFormat::Sha1,
10731                &[PktLineFrame::Data(
10732                    b"want 1111111111111111111111111111111111111111\n".to_vec(),
10733                )],
10734            )
10735            .is_err()
10736        );
10737        assert!(parse_upload_pack_negotiation_request(
10738            ObjectFormat::Sha1,
10739            &[
10740                PktLineFrame::Data(b"done\n".to_vec()),
10741                PktLineFrame::Data(b"have 1111111111111111111111111111111111111111\n".to_vec(),),
10742            ],
10743        )
10744        .is_err());
10745        assert!(
10746            parse_upload_pack_negotiation_request(
10747                ObjectFormat::Sha1,
10748                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
10749            )
10750            .is_err()
10751        );
10752    }
10753
10754    #[test]
10755    fn upload_pack_acknowledgments_parse_and_encode_statuses() {
10756        let oid = ObjectId::from_hex(
10757            ObjectFormat::Sha1,
10758            "1111111111111111111111111111111111111111",
10759        )
10760        .expect("test operation should succeed");
10761        assert_eq!(
10762            parse_upload_pack_acknowledgment(ObjectFormat::Sha1, b"NAK\n")
10763                .expect("test operation should succeed"),
10764            UploadPackAcknowledgment::Nak
10765        );
10766        for (payload, status) in [
10767            (
10768                b"ACK 1111111111111111111111111111111111111111\n".as_slice(),
10769                None,
10770            ),
10771            (
10772                b"ACK 1111111111111111111111111111111111111111 continue\n".as_slice(),
10773                Some(UploadPackAckStatus::Continue),
10774            ),
10775            (
10776                b"ACK 1111111111111111111111111111111111111111 common\n".as_slice(),
10777                Some(UploadPackAckStatus::Common),
10778            ),
10779            (
10780                b"ACK 1111111111111111111111111111111111111111 ready\n".as_slice(),
10781                Some(UploadPackAckStatus::Ready),
10782            ),
10783        ] {
10784            let acknowledgment = parse_upload_pack_acknowledgment(ObjectFormat::Sha1, payload)
10785                .expect("test operation should succeed");
10786            assert_eq!(
10787                acknowledgment,
10788                UploadPackAcknowledgment::Ack { oid, status }
10789            );
10790            assert_eq!(
10791                encode_upload_pack_acknowledgment(&acknowledgment)
10792                    .expect("test operation should succeed"),
10793                payload
10794            );
10795        }
10796    }
10797
10798    #[test]
10799    fn upload_pack_acknowledgments_stream_round_trip_and_reject_bad_lines() {
10800        let acknowledgment = UploadPackAcknowledgment::Ack {
10801            oid: ObjectId::from_hex(
10802                ObjectFormat::Sha1,
10803                "1111111111111111111111111111111111111111",
10804            )
10805            .expect("test operation should succeed"),
10806            status: Some(UploadPackAckStatus::Ready),
10807        };
10808        let mut encoded = Vec::new();
10809        write_upload_pack_acknowledgment(&mut encoded, &acknowledgment)
10810            .expect("test operation should succeed");
10811        encoded.extend_from_slice(b"tail");
10812
10813        let mut input = encoded.as_slice();
10814        assert_eq!(
10815            read_upload_pack_acknowledgment(ObjectFormat::Sha1, &mut input)
10816                .expect("test operation should succeed"),
10817            acknowledgment
10818        );
10819        assert_eq!(input, b"tail");
10820        assert!(parse_upload_pack_acknowledgment(ObjectFormat::Sha1, b"ACK not-an-oid\n").is_err());
10821        assert!(
10822            parse_upload_pack_acknowledgment(
10823                ObjectFormat::Sha1,
10824                b"ACK 1111111111111111111111111111111111111111 unknown\n",
10825            )
10826            .is_err()
10827        );
10828        assert!(
10829            parse_upload_pack_acknowledgment(
10830                ObjectFormat::Sha1,
10831                b"ACK 1111111111111111111111111111111111111111 ready extra\n",
10832            )
10833            .is_err()
10834        );
10835        assert!(
10836            parse_upload_pack_acknowledgment(ObjectFormat::Sha1, b"ERR remote died\n").is_err()
10837        );
10838        assert!(read_upload_pack_acknowledgment(ObjectFormat::Sha1, &mut &b"0000"[..]).is_err());
10839    }
10840
10841    #[test]
10842    fn upload_pack_packfile_response_parses_acknowledgments_and_sideband() {
10843        let oid = ObjectId::from_hex(
10844            ObjectFormat::Sha1,
10845            "1111111111111111111111111111111111111111",
10846        )
10847        .expect("test operation should succeed");
10848        let frames = vec![
10849            PktLineFrame::Data(b"ACK 1111111111111111111111111111111111111111 common\n".to_vec()),
10850            PktLineFrame::Data(b"NAK\n".to_vec()),
10851            PktLineFrame::Data(b"\x01PACK".to_vec()),
10852            PktLineFrame::Data(b"\x02counting objects\n".to_vec()),
10853            PktLineFrame::Data(b"\x01 bytes".to_vec()),
10854            PktLineFrame::Flush,
10855        ];
10856        let response = parse_upload_pack_packfile_response(ObjectFormat::Sha1, &frames)
10857            .expect("test operation should succeed");
10858        assert_eq!(
10859            response,
10860            UploadPackPackfileResponse {
10861                acknowledgments: vec![
10862                    UploadPackAcknowledgment::Ack {
10863                        oid,
10864                        status: Some(UploadPackAckStatus::Common),
10865                    },
10866                    UploadPackAcknowledgment::Nak,
10867                ],
10868                sideband: vec![
10869                    SideBandPacket {
10870                        channel: SideBandChannel::Data,
10871                        data: b"PACK".to_vec(),
10872                    },
10873                    SideBandPacket {
10874                        channel: SideBandChannel::Progress,
10875                        data: b"counting objects\n".to_vec(),
10876                    },
10877                    SideBandPacket {
10878                        channel: SideBandChannel::Data,
10879                        data: b" bytes".to_vec(),
10880                    },
10881                ],
10882            }
10883        );
10884        assert_eq!(
10885            demux_upload_pack_packfile_response(&response).expect("test operation should succeed"),
10886            SideBandDemux {
10887                data: b"PACK bytes".to_vec(),
10888                progress: vec![b"counting objects\n".to_vec()],
10889            }
10890        );
10891        assert_eq!(
10892            encode_upload_pack_packfile_response(&response).expect("test operation should succeed"),
10893            frames
10894        );
10895    }
10896
10897    #[test]
10898    fn upload_pack_packfile_response_streams_round_trip() {
10899        let response = UploadPackPackfileResponse {
10900            acknowledgments: vec![UploadPackAcknowledgment::Nak],
10901            sideband: vec![SideBandPacket {
10902                channel: SideBandChannel::Data,
10903                data: b"PACK".to_vec(),
10904            }],
10905        };
10906        let mut encoded = Vec::new();
10907        write_upload_pack_packfile_response(&mut encoded, &response)
10908            .expect("test operation should succeed");
10909        encoded.extend_from_slice(b"tail");
10910
10911        let mut input = encoded.as_slice();
10912        assert_eq!(
10913            read_upload_pack_packfile_response(ObjectFormat::Sha1, &mut input)
10914                .expect("test operation should succeed"),
10915            response
10916        );
10917        assert_eq!(input, b"tail");
10918    }
10919
10920    #[test]
10921    fn upload_pack_packfile_response_rejects_malformed_streams() {
10922        assert!(
10923            parse_upload_pack_packfile_response(
10924                ObjectFormat::Sha1,
10925                &[PktLineFrame::Data(b"NAK\n".to_vec())],
10926            )
10927            .is_err()
10928        );
10929        assert!(
10930            parse_upload_pack_packfile_response(
10931                ObjectFormat::Sha1,
10932                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
10933            )
10934            .is_err()
10935        );
10936        assert!(
10937            parse_upload_pack_packfile_response(
10938                ObjectFormat::Sha1,
10939                &[
10940                    PktLineFrame::Data(b"\x01PACK".to_vec()),
10941                    PktLineFrame::Data(
10942                        b"ACK 1111111111111111111111111111111111111111 common\n".to_vec()
10943                    ),
10944                    PktLineFrame::Flush,
10945                ],
10946            )
10947            .is_err()
10948        );
10949        assert!(
10950            parse_upload_pack_packfile_response(
10951                ObjectFormat::Sha1,
10952                &[
10953                    PktLineFrame::Data(b"NAK\n".to_vec()),
10954                    PktLineFrame::Flush,
10955                    PktLineFrame::Data(b"\x01PACK".to_vec()),
10956                ],
10957            )
10958            .is_err()
10959        );
10960        assert!(
10961            parse_upload_pack_packfile_response(
10962                ObjectFormat::Sha1,
10963                &[
10964                    PktLineFrame::Data(b"NAK\n".to_vec()),
10965                    PktLineFrame::Data(b"\x04bad".to_vec()),
10966                    PktLineFrame::Flush,
10967                ],
10968            )
10969            .is_err()
10970        );
10971    }
10972
10973    #[test]
10974    fn upload_pack_raw_packfile_response_parses_acknowledgments_and_raw_pack() {
10975        let oid = ObjectId::from_hex(
10976            ObjectFormat::Sha1,
10977            "1111111111111111111111111111111111111111",
10978        )
10979        .expect("test operation should succeed");
10980        let response = UploadPackRawPackfileResponse {
10981            acknowledgments: vec![
10982                UploadPackAcknowledgment::Ack {
10983                    oid,
10984                    status: Some(UploadPackAckStatus::Common),
10985                },
10986                UploadPackAcknowledgment::Nak,
10987            ],
10988            packfile: b"PACK\x00\x00\x00\x02raw-bytes".to_vec(),
10989        };
10990        let encoded = encode_upload_pack_raw_packfile_response(&response)
10991            .expect("test operation should succeed");
10992        assert_eq!(
10993            parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &encoded)
10994                .expect("test operation should succeed"),
10995            response
10996        );
10997    }
10998
10999    #[test]
11000    fn upload_pack_raw_packfile_response_streams_round_trip() {
11001        let response = UploadPackRawPackfileResponse {
11002            acknowledgments: vec![UploadPackAcknowledgment::Nak],
11003            packfile: b"PACK\x00\x00\x00\x02raw-bytes".to_vec(),
11004        };
11005        let mut encoded = Vec::new();
11006        write_upload_pack_raw_packfile_response(&mut encoded, &response)
11007            .expect("test operation should succeed");
11008        assert_eq!(
11009            encoded,
11010            encode_upload_pack_raw_packfile_response(&response)
11011                .expect("test operation should succeed")
11012        );
11013
11014        let mut input = encoded.as_slice();
11015        assert_eq!(
11016            read_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &mut input)
11017                .expect("test operation should succeed"),
11018            response
11019        );
11020        assert!(input.is_empty());
11021    }
11022
11023    #[test]
11024    fn upload_pack_raw_packfile_response_rejects_malformed_streams() {
11025        let ack = PktLineFrame::data(b"NAK\n".to_vec())
11026            .expect("test operation should succeed")
11027            .try_encode()
11028            .expect("test operation should succeed");
11029        let bad_ack = PktLineFrame::data(b"ACK not-an-oid\n".to_vec())
11030            .expect("test operation should succeed")
11031            .try_encode()
11032            .expect("test operation should succeed");
11033        let non_ack =
11034            PktLineFrame::data(b"have 1111111111111111111111111111111111111111\n".to_vec())
11035                .expect("test operation should succeed")
11036                .try_encode()
11037                .expect("test operation should succeed");
11038        let mut garbage_after_ack = ack.clone();
11039        garbage_after_ack.extend_from_slice(b"garbage");
11040
11041        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, b"").is_err());
11042        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &ack).is_err());
11043        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &bad_ack).is_err());
11044        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, b"0000PACK").is_err());
11045        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &non_ack).is_err());
11046        assert!(
11047            parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &garbage_after_ack)
11048                .is_err()
11049        );
11050        assert!(
11051            encode_upload_pack_raw_packfile_response(&UploadPackRawPackfileResponse {
11052                acknowledgments: vec![UploadPackAcknowledgment::Nak],
11053                packfile: Vec::new(),
11054            })
11055            .is_err()
11056        );
11057        assert!(
11058            encode_upload_pack_raw_packfile_response(&UploadPackRawPackfileResponse {
11059                acknowledgments: Vec::new(),
11060                packfile: b"not-a-pack".to_vec(),
11061            })
11062            .is_err()
11063        );
11064    }
11065
11066    #[test]
11067    fn upload_pack_request_encodes_deepen_request() {
11068        // A `--depth 1` clone over smart-HTTP v1: the `want` line carries the
11069        // capabilities, the client's existing shallow boundary is replayed as a
11070        // `shallow` line, and `deepen 1` requests the truncation. Built as raw
11071        // pkt-line bytes so the 4-hex length prefixes are exercised.
11072        let want = ObjectId::from_hex(
11073            ObjectFormat::Sha1,
11074            "1111111111111111111111111111111111111111",
11075        )
11076        .expect("test operation should succeed");
11077        let boundary = ObjectId::from_hex(
11078            ObjectFormat::Sha1,
11079            "2222222222222222222222222222222222222222",
11080        )
11081        .expect("test operation should succeed");
11082        let request = UploadPackRequest {
11083            wants: vec![want],
11084            capabilities: vec![Capability {
11085                name: "shallow".into(),
11086                value: None,
11087            }],
11088            shallow: vec![boundary],
11089            deepen: Some(1),
11090            ..UploadPackRequest::default()
11091        };
11092        let mut encoded = Vec::new();
11093        write_upload_pack_request(&mut encoded, Some(&request))
11094            .expect("test operation should succeed");
11095        let mut expected = Vec::new();
11096        expected.extend_from_slice(b"003awant 1111111111111111111111111111111111111111 shallow\n");
11097        expected.extend_from_slice(b"0035shallow 2222222222222222222222222222222222222222\n");
11098        expected.extend_from_slice(b"000ddeepen 1\n");
11099        expected.extend_from_slice(b"0000");
11100        assert_eq!(encoded, expected);
11101    }
11102
11103    #[test]
11104    fn upload_pack_shallow_info_response_parses_shallow_unshallow_and_pack() {
11105        // The smart-HTTP v1 deepen response: a shallow-info section (one
11106        // `shallow` and one `unshallow` line) terminated by a flush, then the
11107        // NAK and the raw packfile. Hand-built pkt-lines (mind the lengths).
11108        let shallow = ObjectId::from_hex(
11109            ObjectFormat::Sha1,
11110            "1111111111111111111111111111111111111111",
11111        )
11112        .expect("test operation should succeed");
11113        let unshallow = ObjectId::from_hex(
11114            ObjectFormat::Sha1,
11115            "2222222222222222222222222222222222222222",
11116        )
11117        .expect("test operation should succeed");
11118        let mut input = Vec::new();
11119        input.extend_from_slice(b"0035shallow 1111111111111111111111111111111111111111\n");
11120        input.extend_from_slice(b"0037unshallow 2222222222222222222222222222222222222222\n");
11121        input.extend_from_slice(b"0000"); // shallow-info terminator
11122        input.extend_from_slice(b"0008NAK\n");
11123        input.extend_from_slice(b"PACK\x00\x00\x00\x02raw-bytes");
11124
11125        let (entries, response) =
11126            parse_upload_pack_shallow_info_and_raw_packfile_response(ObjectFormat::Sha1, &input)
11127                .expect("test operation should succeed");
11128        assert_eq!(
11129            entries,
11130            vec![
11131                ProtocolV2FetchShallowInfo::Shallow(shallow),
11132                ProtocolV2FetchShallowInfo::Unshallow(unshallow),
11133            ]
11134        );
11135        assert_eq!(
11136            response,
11137            UploadPackRawPackfileResponse {
11138                acknowledgments: vec![UploadPackAcknowledgment::Nak],
11139                packfile: b"PACK\x00\x00\x00\x02raw-bytes".to_vec(),
11140            }
11141        );
11142
11143        // The reader entry point yields the same result over a stream.
11144        let mut stream = input.as_slice();
11145        let (read_entries, read_response) =
11146            read_upload_pack_shallow_info_and_raw_packfile_response(
11147                ObjectFormat::Sha1,
11148                &mut stream,
11149            )
11150            .expect("test operation should succeed");
11151        assert_eq!(read_entries, entries);
11152        assert_eq!(read_response, response);
11153    }
11154
11155    #[test]
11156    fn upload_pack_shallow_info_response_handles_empty_shallow_section() {
11157        // A deepen request that creates no boundary change still gets an empty
11158        // shallow-info section (a bare flush) before the NAK + pack.
11159        let mut input = Vec::new();
11160        input.extend_from_slice(b"0000"); // empty shallow-info
11161        input.extend_from_slice(b"0008NAK\n");
11162        input.extend_from_slice(b"PACK\x00\x00\x00\x02raw-bytes");
11163
11164        let (entries, response) =
11165            parse_upload_pack_shallow_info_and_raw_packfile_response(ObjectFormat::Sha1, &input)
11166                .expect("test operation should succeed");
11167        assert!(entries.is_empty());
11168        assert_eq!(
11169            response.acknowledgments,
11170            vec![UploadPackAcknowledgment::Nak]
11171        );
11172        assert!(response.packfile.starts_with(b"PACK"));
11173    }
11174
11175    #[test]
11176    fn upload_pack_shallow_info_response_rejects_malformed_sections() {
11177        // Truncated section (no terminating flush before EOF).
11178        let truncated = b"0035shallow 1111111111111111111111111111111111111111\n".to_vec();
11179        assert!(
11180            parse_upload_pack_shallow_info_and_raw_packfile_response(
11181                ObjectFormat::Sha1,
11182                &truncated
11183            )
11184            .is_err()
11185        );
11186        // A non-flush control packet inside the shallow-info section.
11187        let mut delimiter_section = Vec::new();
11188        delimiter_section.extend_from_slice(b"0001"); // delimiter, not a flush
11189        assert!(
11190            parse_upload_pack_shallow_info_section(ObjectFormat::Sha1, &delimiter_section).is_err()
11191        );
11192        // A non-shallow data line inside the section.
11193        let mut bad_line = Vec::new();
11194        bad_line.extend_from_slice(b"0008NAK\n");
11195        assert!(parse_upload_pack_shallow_info_section(ObjectFormat::Sha1, &bad_line).is_err());
11196        // Valid shallow-info but a missing packfile afterwards.
11197        let mut no_pack = Vec::new();
11198        no_pack.extend_from_slice(b"0000"); // empty shallow-info
11199        no_pack.extend_from_slice(b"0008NAK\n");
11200        assert!(
11201            parse_upload_pack_shallow_info_and_raw_packfile_response(ObjectFormat::Sha1, &no_pack)
11202                .is_err()
11203        );
11204    }
11205
11206    #[test]
11207    fn receive_pack_request_parses_and_encodes_commands() {
11208        let old_id = ObjectId::from_hex(
11209            ObjectFormat::Sha1,
11210            "1111111111111111111111111111111111111111",
11211        )
11212        .expect("test operation should succeed");
11213        let new_id = ObjectId::from_hex(
11214            ObjectFormat::Sha1,
11215            "2222222222222222222222222222222222222222",
11216        )
11217        .expect("test operation should succeed");
11218        let delete_old_id = ObjectId::from_hex(
11219            ObjectFormat::Sha1,
11220            "3333333333333333333333333333333333333333",
11221        )
11222        .expect("test operation should succeed");
11223        let zero = ObjectId::from_hex(
11224            ObjectFormat::Sha1,
11225            "0000000000000000000000000000000000000000",
11226        )
11227        .expect("test operation should succeed");
11228        let shallow = ObjectId::from_hex(
11229            ObjectFormat::Sha1,
11230            "4444444444444444444444444444444444444444",
11231        )
11232        .expect("test operation should succeed");
11233        let frames = vec![
11234            PktLineFrame::Data(b"shallow 4444444444444444444444444444444444444444\n".to_vec()),
11235            PktLineFrame::Data(
11236                b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\0report-status side-band-64k agent=git/2.54.0\n"
11237                    .to_vec(),
11238            ),
11239            PktLineFrame::Data(
11240                b"3333333333333333333333333333333333333333 0000000000000000000000000000000000000000 refs/heads/old\n"
11241                    .to_vec(),
11242            ),
11243            PktLineFrame::Flush,
11244        ];
11245        let request = parse_receive_pack_request(ObjectFormat::Sha1, &frames)
11246            .expect("test operation should succeed");
11247        assert_eq!(
11248            request,
11249            ReceivePackRequest {
11250                shallow: vec![shallow],
11251                commands: vec![
11252                    ReceivePackCommand {
11253                        old_id,
11254                        new_id,
11255                        name: "refs/heads/main".into(),
11256                    },
11257                    ReceivePackCommand {
11258                        old_id: delete_old_id,
11259                        new_id: zero,
11260                        name: "refs/heads/old".into(),
11261                    },
11262                ],
11263                capabilities: vec![
11264                    Capability {
11265                        name: "report-status".into(),
11266                        value: None,
11267                    },
11268                    Capability {
11269                        name: "side-band-64k".into(),
11270                        value: None,
11271                    },
11272                    Capability {
11273                        name: "agent".into(),
11274                        value: Some("git/2.54.0".into()),
11275                    },
11276                ],
11277            }
11278        );
11279        assert_eq!(
11280            encode_receive_pack_request(&request).expect("test operation should succeed"),
11281            frames
11282        );
11283        assert_eq!(
11284            parse_receive_pack_request(ObjectFormat::Sha1, &[PktLineFrame::Flush])
11285                .expect("test operation should succeed"),
11286            ReceivePackRequest::default()
11287        );
11288    }
11289
11290    #[test]
11291    fn receive_pack_request_streams_round_trip() {
11292        let request = ReceivePackRequest {
11293            commands: vec![ReceivePackCommand {
11294                old_id: ObjectId::from_hex(
11295                    ObjectFormat::Sha1,
11296                    "0000000000000000000000000000000000000000",
11297                )
11298                .expect("test operation should succeed"),
11299                new_id: ObjectId::from_hex(
11300                    ObjectFormat::Sha1,
11301                    "1111111111111111111111111111111111111111",
11302                )
11303                .expect("test operation should succeed"),
11304                name: "refs/heads/main".into(),
11305            }],
11306            capabilities: vec![Capability {
11307                name: "report-status".into(),
11308                value: None,
11309            }],
11310            ..ReceivePackRequest::default()
11311        };
11312        let mut encoded = Vec::new();
11313        write_receive_pack_request(&mut encoded, &request).expect("test operation should succeed");
11314        encoded.extend_from_slice(b"PACK");
11315
11316        let mut input = encoded.as_slice();
11317        assert_eq!(
11318            read_receive_pack_request(ObjectFormat::Sha1, &mut input)
11319                .expect("test operation should succeed"),
11320            request
11321        );
11322        assert_eq!(input, b"PACK");
11323    }
11324
11325    #[test]
11326    fn receive_pack_request_rejects_malformed_commands() {
11327        assert!(
11328            parse_receive_pack_request(
11329                ObjectFormat::Sha1,
11330                &[PktLineFrame::Data(
11331                    b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\n"
11332                        .to_vec(),
11333                )],
11334            )
11335            .is_err()
11336        );
11337        assert!(
11338            parse_receive_pack_request(
11339                ObjectFormat::Sha1,
11340                &[
11341                    PktLineFrame::Data(
11342                        b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\n"
11343                            .to_vec(),
11344                    ),
11345                    PktLineFrame::Data(
11346                        b"shallow 3333333333333333333333333333333333333333\n".to_vec(),
11347                    ),
11348                    PktLineFrame::Flush,
11349                ],
11350            )
11351            .is_err()
11352        );
11353        assert!(
11354            parse_receive_pack_request(
11355                ObjectFormat::Sha1,
11356                &[
11357                    PktLineFrame::Data(
11358                        b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\0report-status\n"
11359                            .to_vec(),
11360                    ),
11361                    PktLineFrame::Data(
11362                        b"3333333333333333333333333333333333333333 4444444444444444444444444444444444444444 refs/heads/next\0side-band-64k\n"
11363                            .to_vec(),
11364                    ),
11365                    PktLineFrame::Flush,
11366                ],
11367            )
11368            .is_err()
11369        );
11370        assert!(
11371            parse_receive_pack_request(
11372                ObjectFormat::Sha1,
11373                &[
11374                    PktLineFrame::Data(
11375                        b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec(),
11376                    ),
11377                    PktLineFrame::Flush,
11378                ],
11379            )
11380            .is_err()
11381        );
11382        assert!(
11383            encode_receive_pack_request(&ReceivePackRequest {
11384                shallow: vec![
11385                    ObjectId::from_hex(
11386                        ObjectFormat::Sha1,
11387                        "1111111111111111111111111111111111111111",
11388                    )
11389                    .expect("test operation should succeed")
11390                ],
11391                ..ReceivePackRequest::default()
11392            })
11393            .is_err()
11394        );
11395        assert!(
11396            encode_receive_pack_request(&ReceivePackRequest {
11397                commands: vec![ReceivePackCommand {
11398                    old_id: ObjectId::from_hex(
11399                        ObjectFormat::Sha1,
11400                        "1111111111111111111111111111111111111111",
11401                    )
11402                    .expect("test operation should succeed"),
11403                    new_id: ObjectId::from_hex(
11404                        ObjectFormat::Sha1,
11405                        "2222222222222222222222222222222222222222",
11406                    )
11407                    .expect("test operation should succeed"),
11408                    name: "bad ref".into(),
11409                }],
11410                ..ReceivePackRequest::default()
11411            })
11412            .is_err()
11413        );
11414    }
11415
11416    #[test]
11417    fn receive_pack_features_parse_encode_and_validate_push_request() {
11418        let capabilities = vec![
11419            Capability {
11420                name: "report-status".into(),
11421                value: None,
11422            },
11423            Capability {
11424                name: "report-status-v2".into(),
11425                value: None,
11426            },
11427            Capability {
11428                name: "delete-refs".into(),
11429                value: None,
11430            },
11431            Capability {
11432                name: "ofs-delta".into(),
11433                value: None,
11434            },
11435            Capability {
11436                name: "atomic".into(),
11437                value: None,
11438            },
11439            Capability {
11440                name: "push-options".into(),
11441                value: None,
11442            },
11443            Capability {
11444                name: "side-band-64k".into(),
11445                value: None,
11446            },
11447            Capability {
11448                name: "quiet".into(),
11449                value: None,
11450            },
11451            Capability {
11452                name: "no-thin".into(),
11453                value: None,
11454            },
11455            Capability {
11456                name: "agent".into(),
11457                value: Some("git/2.54.0".into()),
11458            },
11459            Capability {
11460                name: "object-format".into(),
11461                value: Some("sha256".into()),
11462            },
11463            Capability {
11464                name: "custom".into(),
11465                value: Some("value".into()),
11466            },
11467        ];
11468        let features =
11469            parse_receive_pack_features(&capabilities).expect("test operation should succeed");
11470        assert_eq!(
11471            features,
11472            ReceivePackFeatures {
11473                report_status: true,
11474                report_status_v2: true,
11475                delete_refs: true,
11476                ofs_delta: true,
11477                atomic: true,
11478                push_options: true,
11479                side_band_64k: true,
11480                quiet: true,
11481                no_thin: true,
11482                agent: Some("git/2.54.0".into()),
11483                object_format: Some(ObjectFormat::Sha256),
11484                unknown: vec![Capability {
11485                    name: "custom".into(),
11486                    value: Some("value".into()),
11487                }],
11488            }
11489        );
11490        assert_eq!(
11491            encode_receive_pack_features(&features).expect("test operation should succeed"),
11492            capabilities
11493        );
11494
11495        let request = ReceivePackPushRequest {
11496            commands: ReceivePackRequest {
11497                commands: vec![ReceivePackCommand {
11498                    old_id: ObjectId::from_hex(
11499                        ObjectFormat::Sha1,
11500                        "1111111111111111111111111111111111111111",
11501                    )
11502                    .expect("test operation should succeed"),
11503                    new_id: ObjectId::from_hex(
11504                        ObjectFormat::Sha1,
11505                        "2222222222222222222222222222222222222222",
11506                    )
11507                    .expect("test operation should succeed"),
11508                    name: "refs/heads/main".into(),
11509                }],
11510                capabilities: vec![
11511                    Capability {
11512                        name: "report-status".into(),
11513                        value: None,
11514                    },
11515                    Capability {
11516                        name: "ofs-delta".into(),
11517                        value: None,
11518                    },
11519                    Capability {
11520                        name: "push-options".into(),
11521                        value: None,
11522                    },
11523                    Capability {
11524                        name: "side-band-64k".into(),
11525                        value: None,
11526                    },
11527                    Capability {
11528                        name: "agent".into(),
11529                        value: Some("sley".into()),
11530                    },
11531                ],
11532                ..ReceivePackRequest::default()
11533            },
11534            push_options: Some(vec!["ci.skip".into()]),
11535            packfile: b"PACKpayload".to_vec(),
11536        };
11537        validate_receive_pack_push_request_features(&features, &request)
11538            .expect("test operation should succeed");
11539    }
11540
11541    #[test]
11542    fn receive_pack_features_reject_invalid_push_requests() {
11543        let old_id = ObjectId::from_hex(
11544            ObjectFormat::Sha1,
11545            "1111111111111111111111111111111111111111",
11546        )
11547        .expect("test operation should succeed");
11548        let new_id = ObjectId::from_hex(
11549            ObjectFormat::Sha1,
11550            "2222222222222222222222222222222222222222",
11551        )
11552        .expect("test operation should succeed");
11553        let zero = ObjectId::from_hex(
11554            ObjectFormat::Sha1,
11555            "0000000000000000000000000000000000000000",
11556        )
11557        .expect("test operation should succeed");
11558        let features = ReceivePackFeatures {
11559            report_status: true,
11560            push_options: true,
11561            ..ReceivePackFeatures::default()
11562        };
11563        let update = ReceivePackCommand {
11564            old_id: old_id.clone(),
11565            new_id: new_id.clone(),
11566            name: "refs/heads/main".into(),
11567        };
11568
11569        assert!(
11570            validate_receive_pack_push_request_features(
11571                &features,
11572                &ReceivePackPushRequest {
11573                    commands: ReceivePackRequest {
11574                        commands: vec![update.clone()],
11575                        capabilities: vec![Capability {
11576                            name: "push-options".into(),
11577                            value: None,
11578                        }],
11579                        ..ReceivePackRequest::default()
11580                    },
11581                    push_options: None,
11582                    packfile: b"PACKpayload".to_vec(),
11583                },
11584            )
11585            .is_err()
11586        );
11587        assert!(
11588            validate_receive_pack_push_request_features(
11589                &features,
11590                &ReceivePackPushRequest {
11591                    commands: ReceivePackRequest {
11592                        commands: vec![update.clone()],
11593                        ..ReceivePackRequest::default()
11594                    },
11595                    push_options: Some(Vec::new()),
11596                    packfile: b"PACKpayload".to_vec(),
11597                },
11598            )
11599            .is_err()
11600        );
11601        assert!(
11602            validate_receive_pack_push_request_features(
11603                &features,
11604                &ReceivePackPushRequest {
11605                    commands: ReceivePackRequest {
11606                        commands: vec![ReceivePackCommand {
11607                            old_id: old_id.clone(),
11608                            new_id: zero.clone(),
11609                            name: "refs/heads/main".into(),
11610                        }],
11611                        ..ReceivePackRequest::default()
11612                    },
11613                    push_options: None,
11614                    packfile: Vec::new(),
11615                },
11616            )
11617            .is_err()
11618        );
11619        assert!(
11620            validate_receive_pack_push_request_features(
11621                &features,
11622                &ReceivePackPushRequest {
11623                    commands: ReceivePackRequest {
11624                        commands: vec![update.clone()],
11625                        ..ReceivePackRequest::default()
11626                    },
11627                    push_options: None,
11628                    packfile: Vec::new(),
11629                },
11630            )
11631            .is_err()
11632        );
11633        assert!(
11634            validate_receive_pack_push_request_features(
11635                &ReceivePackFeatures {
11636                    delete_refs: true,
11637                    ..ReceivePackFeatures::default()
11638                },
11639                &ReceivePackPushRequest {
11640                    commands: ReceivePackRequest {
11641                        commands: vec![ReceivePackCommand {
11642                            old_id,
11643                            new_id: zero,
11644                            name: "refs/heads/main".into(),
11645                        }],
11646                        ..ReceivePackRequest::default()
11647                    },
11648                    push_options: None,
11649                    packfile: b"PACKpayload".to_vec(),
11650                },
11651            )
11652            .is_err()
11653        );
11654        assert!(
11655            validate_receive_pack_push_request_features(
11656                &features,
11657                &ReceivePackPushRequest {
11658                    commands: ReceivePackRequest {
11659                        commands: vec![update],
11660                        capabilities: vec![Capability {
11661                            name: "atomic".into(),
11662                            value: None,
11663                        }],
11664                        ..ReceivePackRequest::default()
11665                    },
11666                    push_options: None,
11667                    packfile: b"PACKpayload".to_vec(),
11668                },
11669            )
11670            .is_err()
11671        );
11672
11673        assert!(
11674            parse_receive_pack_features(&[
11675                Capability {
11676                    name: "push-options".into(),
11677                    value: None,
11678                },
11679                Capability {
11680                    name: "push-options".into(),
11681                    value: None,
11682                },
11683            ])
11684            .is_err()
11685        );
11686        assert!(
11687            encode_receive_pack_features(&ReceivePackFeatures {
11688                unknown: vec![Capability {
11689                    name: "atomic".into(),
11690                    value: None,
11691                }],
11692                ..ReceivePackFeatures::default()
11693            })
11694            .is_err()
11695        );
11696    }
11697
11698    #[test]
11699    fn receive_pack_apply_helper_installs_pack_verifies_objects_and_reports_ok() {
11700        let old_id = ObjectId::from_hex(
11701            ObjectFormat::Sha1,
11702            "1111111111111111111111111111111111111111",
11703        )
11704        .expect("test operation should succeed");
11705        let new_id = ObjectId::from_hex(
11706            ObjectFormat::Sha1,
11707            "2222222222222222222222222222222222222222",
11708        )
11709        .expect("test operation should succeed");
11710        let request = ReceivePackPushRequest {
11711            commands: ReceivePackRequest {
11712                commands: vec![ReceivePackCommand {
11713                    old_id: old_id.clone(),
11714                    new_id: new_id.clone(),
11715                    name: "refs/heads/main".into(),
11716                }],
11717                ..ReceivePackRequest::default()
11718            },
11719            packfile: b"PACKpayload".to_vec(),
11720            ..ReceivePackPushRequest::default()
11721        };
11722        let installed = std::cell::Cell::new(false);
11723        let applied = std::cell::RefCell::new(Vec::new());
11724
11725        let report = apply_receive_pack_push_request(
11726            &ReceivePackFeatures::default(),
11727            &request,
11728            |_| unreachable!("update stale-old checks belong to the ref transaction callback"),
11729            |packfile| {
11730                assert_eq!(packfile, b"PACKpayload");
11731                installed.set(true);
11732                Ok(())
11733            },
11734            |oid| Ok(oid == &new_id),
11735            |commands| {
11736                applied.borrow_mut().extend_from_slice(commands);
11737                Ok(())
11738            },
11739            |_| unreachable!("no delete command should be applied"),
11740        )
11741        .expect("test operation should succeed");
11742
11743        assert!(installed.get());
11744        assert_eq!(applied.into_inner(), request.commands.commands);
11745        assert_eq!(report.unpack, ReceivePackUnpackStatus::Ok);
11746        assert_eq!(
11747            report.commands,
11748            vec![ReceivePackCommandStatus::Ok {
11749                name: "refs/heads/main".into(),
11750            }]
11751        );
11752    }
11753
11754    #[test]
11755    fn receive_pack_apply_helper_preserves_delete_only_and_stale_delete_rules() {
11756        let old_id = ObjectId::from_hex(
11757            ObjectFormat::Sha1,
11758            "1111111111111111111111111111111111111111",
11759        )
11760        .expect("test operation should succeed");
11761        let other_id = ObjectId::from_hex(
11762            ObjectFormat::Sha1,
11763            "2222222222222222222222222222222222222222",
11764        )
11765        .expect("test operation should succeed");
11766        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
11767        let request = ReceivePackPushRequest {
11768            commands: ReceivePackRequest {
11769                commands: vec![ReceivePackCommand {
11770                    old_id: old_id.clone(),
11771                    new_id: zero,
11772                    name: "refs/heads/main".into(),
11773                }],
11774                ..ReceivePackRequest::default()
11775            },
11776            ..ReceivePackPushRequest::default()
11777        };
11778        let features = ReceivePackFeatures {
11779            delete_refs: true,
11780            ..ReceivePackFeatures::default()
11781        };
11782        let installed = std::cell::Cell::new(false);
11783        let deleted = std::cell::RefCell::new(Vec::new());
11784
11785        let report = apply_receive_pack_push_request(
11786            &features,
11787            &request,
11788            |_| Ok(Some(old_id.clone())),
11789            |_| {
11790                installed.set(true);
11791                Ok(())
11792            },
11793            |_| Ok(false),
11794            |_| unreachable!("delete-only request should not apply updates"),
11795            |command| {
11796                deleted.borrow_mut().push(command.name.clone());
11797                Ok(())
11798            },
11799        )
11800        .expect("test operation should succeed");
11801
11802        assert!(!installed.get());
11803        assert_eq!(deleted.into_inner(), vec!["refs/heads/main"]);
11804        assert_eq!(report.unpack, ReceivePackUnpackStatus::Ok);
11805        assert!(
11806            apply_receive_pack_push_request(
11807                &features,
11808                &request,
11809                |_| Ok(Some(other_id.clone())),
11810                |_| Ok(()),
11811                |_| Ok(false),
11812                |_| Ok(()),
11813                |_| Ok(()),
11814            )
11815            .is_err()
11816        );
11817    }
11818
11819    #[test]
11820    fn receive_pack_push_request_parses_commands_options_and_packfile() {
11821        let command = ReceivePackCommand {
11822            old_id: ObjectId::from_hex(
11823                ObjectFormat::Sha1,
11824                "1111111111111111111111111111111111111111",
11825            )
11826            .expect("test operation should succeed"),
11827            new_id: ObjectId::from_hex(
11828                ObjectFormat::Sha1,
11829                "2222222222222222222222222222222222222222",
11830            )
11831            .expect("test operation should succeed"),
11832            name: "refs/heads/main".into(),
11833        };
11834        let expected = ReceivePackPushRequest {
11835            commands: ReceivePackRequest {
11836                commands: vec![command],
11837                capabilities: vec![
11838                    Capability {
11839                        name: "report-status".into(),
11840                        value: None,
11841                    },
11842                    Capability {
11843                        name: "push-options".into(),
11844                        value: None,
11845                    },
11846                ],
11847                ..ReceivePackRequest::default()
11848            },
11849            push_options: Some(vec!["ci.skip".into(), "deploy=staging".into()]),
11850            packfile: b"PACK\x00\x00\x00\x02payload".to_vec(),
11851        };
11852        let encoded =
11853            encode_receive_pack_push_request(&expected).expect("test operation should succeed");
11854
11855        assert_eq!(
11856            parse_receive_pack_push_request(ObjectFormat::Sha1, &encoded, true)
11857                .expect("test operation should succeed"),
11858            expected
11859        );
11860    }
11861
11862    #[test]
11863    fn receive_pack_push_request_preserves_packfile_without_push_options() {
11864        let request = ReceivePackPushRequest {
11865            commands: ReceivePackRequest {
11866                commands: vec![ReceivePackCommand {
11867                    old_id: ObjectId::from_hex(
11868                        ObjectFormat::Sha1,
11869                        "1111111111111111111111111111111111111111",
11870                    )
11871                    .expect("test operation should succeed"),
11872                    new_id: ObjectId::from_hex(
11873                        ObjectFormat::Sha1,
11874                        "2222222222222222222222222222222222222222",
11875                    )
11876                    .expect("test operation should succeed"),
11877                    name: "refs/heads/main".into(),
11878                }],
11879                ..ReceivePackRequest::default()
11880            },
11881            push_options: None,
11882            packfile: b"0000PACK-like bytes stay raw".to_vec(),
11883        };
11884        let encoded =
11885            encode_receive_pack_push_request(&request).expect("test operation should succeed");
11886
11887        assert_eq!(
11888            parse_receive_pack_push_request(ObjectFormat::Sha1, &encoded, false)
11889                .expect("test operation should succeed"),
11890            request
11891        );
11892    }
11893
11894    #[test]
11895    fn receive_pack_push_request_streams_round_trip() {
11896        let request = ReceivePackPushRequest {
11897            commands: ReceivePackRequest {
11898                commands: vec![ReceivePackCommand {
11899                    old_id: ObjectId::from_hex(
11900                        ObjectFormat::Sha1,
11901                        "1111111111111111111111111111111111111111",
11902                    )
11903                    .expect("test operation should succeed"),
11904                    new_id: ObjectId::from_hex(
11905                        ObjectFormat::Sha1,
11906                        "2222222222222222222222222222222222222222",
11907                    )
11908                    .expect("test operation should succeed"),
11909                    name: "refs/heads/main".into(),
11910                }],
11911                capabilities: vec![Capability {
11912                    name: "push-options".into(),
11913                    value: None,
11914                }],
11915                ..ReceivePackRequest::default()
11916            },
11917            push_options: Some(Vec::new()),
11918            packfile: b"PACKpayload".to_vec(),
11919        };
11920        let mut encoded = Vec::new();
11921        write_receive_pack_push_request(&mut encoded, &request)
11922            .expect("test operation should succeed");
11923
11924        assert_eq!(
11925            read_receive_pack_push_request(ObjectFormat::Sha1, &mut encoded.as_slice(), true)
11926                .expect("test operation should succeed"),
11927            request
11928        );
11929    }
11930
11931    #[test]
11932    fn receive_pack_push_request_rejects_malformed_sections() {
11933        assert!(
11934            parse_receive_pack_push_request(
11935                ObjectFormat::Sha1,
11936                b"0014not-a-command\n0000PACK",
11937                false,
11938            )
11939            .is_err()
11940        );
11941
11942        let request = ReceivePackPushRequest {
11943            commands: ReceivePackRequest {
11944                commands: vec![ReceivePackCommand {
11945                    old_id: ObjectId::from_hex(
11946                        ObjectFormat::Sha1,
11947                        "1111111111111111111111111111111111111111",
11948                    )
11949                    .expect("test operation should succeed"),
11950                    new_id: ObjectId::from_hex(
11951                        ObjectFormat::Sha1,
11952                        "2222222222222222222222222222222222222222",
11953                    )
11954                    .expect("test operation should succeed"),
11955                    name: "refs/heads/main".into(),
11956                }],
11957                ..ReceivePackRequest::default()
11958            },
11959            push_options: None,
11960            packfile: b"PACKpayload".to_vec(),
11961        };
11962        let encoded =
11963            encode_receive_pack_push_request(&request).expect("test operation should succeed");
11964        assert!(parse_receive_pack_push_request(ObjectFormat::Sha1, &encoded, true).is_err());
11965
11966        assert!(
11967            encode_receive_pack_push_request(&ReceivePackPushRequest {
11968                commands: ReceivePackRequest {
11969                    shallow: vec![
11970                        ObjectId::from_hex(
11971                            ObjectFormat::Sha1,
11972                            "1111111111111111111111111111111111111111",
11973                        )
11974                        .expect("test operation should succeed")
11975                    ],
11976                    ..ReceivePackRequest::default()
11977                },
11978                push_options: None,
11979                packfile: Vec::new(),
11980            })
11981            .is_err()
11982        );
11983    }
11984
11985    #[test]
11986    fn receive_pack_report_status_parses_and_encodes_status_lines() {
11987        let frames = vec![
11988            PktLineFrame::Data(b"unpack ok\n".to_vec()),
11989            PktLineFrame::Data(b"ok refs/heads/main\n".to_vec()),
11990            PktLineFrame::Data(b"ng refs/heads/old non-fast-forward\n".to_vec()),
11991            PktLineFrame::Flush,
11992        ];
11993        let report =
11994            parse_receive_pack_report_status(&frames).expect("test operation should succeed");
11995        assert_eq!(
11996            report,
11997            ReceivePackReportStatus {
11998                unpack: ReceivePackUnpackStatus::Ok,
11999                commands: vec![
12000                    ReceivePackCommandStatus::Ok {
12001                        name: "refs/heads/main".into(),
12002                    },
12003                    ReceivePackCommandStatus::Ng {
12004                        name: "refs/heads/old".into(),
12005                        message: "non-fast-forward".into(),
12006                    },
12007                ],
12008            }
12009        );
12010        assert_eq!(
12011            encode_receive_pack_report_status(&report).expect("test operation should succeed"),
12012            frames
12013        );
12014
12015        let frames = vec![
12016            PktLineFrame::Data(b"unpack pack exceeds maximum size\n".to_vec()),
12017            PktLineFrame::Flush,
12018        ];
12019        assert_eq!(
12020            parse_receive_pack_report_status(&frames).expect("test operation should succeed"),
12021            ReceivePackReportStatus {
12022                unpack: ReceivePackUnpackStatus::Error("pack exceeds maximum size".into()),
12023                commands: Vec::new(),
12024            }
12025        );
12026    }
12027
12028    #[test]
12029    fn receive_pack_report_status_streams_round_trip() {
12030        let report = ReceivePackReportStatus {
12031            unpack: ReceivePackUnpackStatus::Ok,
12032            commands: vec![ReceivePackCommandStatus::Ok {
12033                name: "refs/heads/main".into(),
12034            }],
12035        };
12036        let mut encoded = Vec::new();
12037        write_receive_pack_report_status(&mut encoded, &report)
12038            .expect("test operation should succeed");
12039        encoded.extend_from_slice(b"tail");
12040
12041        let mut input = encoded.as_slice();
12042        assert_eq!(
12043            read_receive_pack_report_status(&mut input).expect("test operation should succeed"),
12044            report
12045        );
12046        assert_eq!(input, b"tail");
12047    }
12048
12049    #[test]
12050    fn receive_pack_report_status_rejects_malformed_status_lines() {
12051        assert!(parse_receive_pack_report_status(&[]).is_err());
12052        assert!(
12053            parse_receive_pack_report_status(&[
12054                PktLineFrame::Data(b"unpack ok\n".to_vec()),
12055                PktLineFrame::Data(b"ok refs/heads/main\n".to_vec()),
12056            ])
12057            .is_err()
12058        );
12059        assert!(
12060            parse_receive_pack_report_status(&[
12061                PktLineFrame::Flush,
12062                PktLineFrame::Data(b"ok refs/heads/main\n".to_vec()),
12063            ])
12064            .is_err()
12065        );
12066        assert!(
12067            parse_receive_pack_report_status(&[
12068                PktLineFrame::Data(b"unpack ok\n".to_vec()),
12069                PktLineFrame::Data(b"bad refs/heads/main\n".to_vec()),
12070                PktLineFrame::Flush,
12071            ])
12072            .is_err()
12073        );
12074        assert!(
12075            parse_receive_pack_report_status(&[
12076                PktLineFrame::Data(b"unpack ok\n".to_vec()),
12077                PktLineFrame::Data(b"ng refs/heads/main\n".to_vec()),
12078                PktLineFrame::Flush,
12079            ])
12080            .is_err()
12081        );
12082        assert!(
12083            encode_receive_pack_report_status(&ReceivePackReportStatus {
12084                unpack: ReceivePackUnpackStatus::Error("".into()),
12085                commands: Vec::new(),
12086            })
12087            .is_err()
12088        );
12089        assert!(
12090            encode_receive_pack_report_status(&ReceivePackReportStatus {
12091                unpack: ReceivePackUnpackStatus::Ok,
12092                commands: vec![ReceivePackCommandStatus::Ok {
12093                    name: "bad ref".into(),
12094                }],
12095            })
12096            .is_err()
12097        );
12098    }
12099
12100    #[test]
12101    fn receive_pack_report_status_v2_parses_and_encodes_options() {
12102        let old_oid = ObjectId::from_hex(
12103            ObjectFormat::Sha1,
12104            "1111111111111111111111111111111111111111",
12105        )
12106        .expect("test operation should succeed");
12107        let new_oid = ObjectId::from_hex(
12108            ObjectFormat::Sha1,
12109            "2222222222222222222222222222222222222222",
12110        )
12111        .expect("test operation should succeed");
12112        let frames = vec![
12113            PktLineFrame::Data(b"unpack ok\n".to_vec()),
12114            PktLineFrame::Data(b"ok refs/for/main\n".to_vec()),
12115            PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12116            PktLineFrame::Data(
12117                b"option old-oid 1111111111111111111111111111111111111111\n".to_vec(),
12118            ),
12119            PktLineFrame::Data(
12120                b"option new-oid 2222222222222222222222222222222222222222\n".to_vec(),
12121            ),
12122            PktLineFrame::Data(b"option forced-update\n".to_vec()),
12123            PktLineFrame::Data(b"ng refs/heads/old rejected by hook\n".to_vec()),
12124            PktLineFrame::Flush,
12125        ];
12126        let report = parse_receive_pack_report_status_v2(ObjectFormat::Sha1, &frames)
12127            .expect("test operation should succeed");
12128        assert_eq!(
12129            report,
12130            ReceivePackReportStatusV2 {
12131                unpack: ReceivePackUnpackStatus::Ok,
12132                commands: vec![
12133                    ReceivePackCommandStatusV2::Ok {
12134                        name: "refs/for/main".into(),
12135                        options: ReceivePackCommandStatusV2Options {
12136                            refname: Some("refs/heads/main".into()),
12137                            old_oid: Some(old_oid),
12138                            new_oid: Some(new_oid),
12139                            forced_update: true,
12140                        },
12141                    },
12142                    ReceivePackCommandStatusV2::Ng {
12143                        name: "refs/heads/old".into(),
12144                        message: "rejected by hook".into(),
12145                    },
12146                ],
12147            }
12148        );
12149        assert_eq!(
12150            encode_receive_pack_report_status_v2(&report).expect("test operation should succeed"),
12151            frames
12152        );
12153    }
12154
12155    #[test]
12156    fn receive_pack_report_status_v2_streams_round_trip() {
12157        let report = ReceivePackReportStatusV2 {
12158            unpack: ReceivePackUnpackStatus::Ok,
12159            commands: vec![ReceivePackCommandStatusV2::Ok {
12160                name: "refs/for/main".into(),
12161                options: ReceivePackCommandStatusV2Options {
12162                    refname: Some("refs/heads/main".into()),
12163                    old_oid: None,
12164                    new_oid: None,
12165                    forced_update: false,
12166                },
12167            }],
12168        };
12169        let mut encoded = Vec::new();
12170        write_receive_pack_report_status_v2(&mut encoded, &report)
12171            .expect("test operation should succeed");
12172        encoded.extend_from_slice(b"tail");
12173
12174        let mut input = encoded.as_slice();
12175        assert_eq!(
12176            read_receive_pack_report_status_v2(ObjectFormat::Sha1, &mut input)
12177                .expect("test operation should succeed"),
12178            report
12179        );
12180        assert_eq!(input, b"tail");
12181    }
12182
12183    #[test]
12184    fn receive_pack_report_status_v2_rejects_malformed_options() {
12185        assert!(parse_receive_pack_report_status_v2(ObjectFormat::Sha1, &[]).is_err());
12186        assert!(
12187            parse_receive_pack_report_status_v2(
12188                ObjectFormat::Sha1,
12189                &[
12190                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12191                    PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12192                    PktLineFrame::Flush,
12193                ],
12194            )
12195            .is_err()
12196        );
12197        assert!(
12198            parse_receive_pack_report_status_v2(
12199                ObjectFormat::Sha1,
12200                &[
12201                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12202                    PktLineFrame::Data(b"ng refs/heads/main rejected\n".to_vec()),
12203                    PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12204                    PktLineFrame::Flush,
12205                ],
12206            )
12207            .is_err()
12208        );
12209        assert!(
12210            parse_receive_pack_report_status_v2(
12211                ObjectFormat::Sha1,
12212                &[
12213                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12214                    PktLineFrame::Data(b"ok refs/for/main\n".to_vec()),
12215                    PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12216                    PktLineFrame::Data(b"option refname refs/heads/next\n".to_vec()),
12217                    PktLineFrame::Flush,
12218                ],
12219            )
12220            .is_err()
12221        );
12222        assert!(
12223            parse_receive_pack_report_status_v2(
12224                ObjectFormat::Sha1,
12225                &[
12226                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12227                    PktLineFrame::Data(b"ok refs/for/main\n".to_vec()),
12228                    PktLineFrame::Data(b"option old-oid not-an-oid\n".to_vec()),
12229                    PktLineFrame::Flush,
12230                ],
12231            )
12232            .is_err()
12233        );
12234        assert!(
12235            encode_receive_pack_report_status_v2(&ReceivePackReportStatusV2 {
12236                unpack: ReceivePackUnpackStatus::Ok,
12237                commands: vec![ReceivePackCommandStatusV2::Ok {
12238                    name: "refs/for/main".into(),
12239                    options: ReceivePackCommandStatusV2Options {
12240                        refname: Some("bad ref".into()),
12241                        ..ReceivePackCommandStatusV2Options::default()
12242                    },
12243                }],
12244            })
12245            .is_err()
12246        );
12247    }
12248
12249    #[test]
12250    fn receive_pack_push_options_parse_and_encode_options() {
12251        let frames = vec![
12252            PktLineFrame::Data(b"ci.skip\n".to_vec()),
12253            PktLineFrame::Data(b"deploy target=staging\n".to_vec()),
12254            PktLineFrame::Data(b"\n".to_vec()),
12255            PktLineFrame::Flush,
12256        ];
12257        let options =
12258            parse_receive_pack_push_options(&frames).expect("test operation should succeed");
12259        assert_eq!(
12260            options,
12261            vec![
12262                "ci.skip".to_string(),
12263                "deploy target=staging".to_string(),
12264                String::new(),
12265            ]
12266        );
12267        assert_eq!(
12268            encode_receive_pack_push_options(&options).expect("test operation should succeed"),
12269            frames
12270        );
12271        assert_eq!(
12272            parse_receive_pack_push_options(&[PktLineFrame::Flush])
12273                .expect("test operation should succeed"),
12274            Vec::<String>::new()
12275        );
12276    }
12277
12278    #[test]
12279    fn receive_pack_push_options_streams_round_trip() {
12280        let options = vec!["ci.skip".to_string(), "reviewer=alice".to_string()];
12281        let mut encoded = Vec::new();
12282        write_receive_pack_push_options(&mut encoded, &options)
12283            .expect("test operation should succeed");
12284        encoded.extend_from_slice(b"PACK");
12285
12286        let mut input = encoded.as_slice();
12287        assert_eq!(
12288            read_receive_pack_push_options(&mut input).expect("test operation should succeed"),
12289            options
12290        );
12291        assert_eq!(input, b"PACK");
12292    }
12293
12294    #[test]
12295    fn receive_pack_push_options_reject_malformed_streams() {
12296        assert!(
12297            parse_receive_pack_push_options(&[PktLineFrame::Data(b"ci.skip\n".to_vec())]).is_err()
12298        );
12299        assert!(
12300            parse_receive_pack_push_options(&[PktLineFrame::Delimiter, PktLineFrame::Flush])
12301                .is_err()
12302        );
12303        assert!(
12304            parse_receive_pack_push_options(&[
12305                PktLineFrame::Data(b"ci.skip\n".to_vec()),
12306                PktLineFrame::Flush,
12307                PktLineFrame::Data(b"after\n".to_vec()),
12308            ])
12309            .is_err()
12310        );
12311        assert!(
12312            parse_receive_pack_push_options(&[
12313                PktLineFrame::Data(b"bad\0option\n".to_vec()),
12314                PktLineFrame::Flush,
12315            ])
12316            .is_err()
12317        );
12318        assert!(encode_receive_pack_push_options(&["bad\noption".to_string()]).is_err());
12319    }
12320
12321    #[test]
12322    fn protocol_v2_advertisement_parses_version_and_capabilities() {
12323        let frames = parse_pkt_line_stream(
12324            b"000eversion 2\n0015agent=git/2.54.0\n0013ls-refs=unborn\n0027fetch=shallow wait-for-done filter\n0012server-option\n0000",
12325        )
12326        .expect("test operation should succeed");
12327        let handshake =
12328            parse_protocol_v2_advertisement(&frames).expect("test operation should succeed");
12329        assert_eq!(handshake.protocol, ProtocolVersion::V2);
12330        assert_eq!(
12331            handshake.capabilities,
12332            vec![
12333                Capability {
12334                    name: "agent".into(),
12335                    value: Some("git/2.54.0".into()),
12336                },
12337                Capability {
12338                    name: "ls-refs".into(),
12339                    value: Some("unborn".into()),
12340                },
12341                Capability {
12342                    name: "fetch".into(),
12343                    value: Some("shallow wait-for-done filter".into()),
12344                },
12345                Capability {
12346                    name: "server-option".into(),
12347                    value: None,
12348                },
12349            ]
12350        );
12351        assert_eq!(
12352            encode_protocol_v2_advertisement(&handshake).expect("test operation should succeed"),
12353            frames
12354        );
12355    }
12356
12357    #[test]
12358    fn protocol_v2_advertisement_reads_until_flush() {
12359        let mut input = b"000eversion 2\n0013ls-refs=unborn\n0000next-session".as_slice();
12360        let handshake =
12361            read_protocol_v2_advertisement(&mut input).expect("test operation should succeed");
12362        assert_eq!(handshake.protocol, ProtocolVersion::V2);
12363        assert_eq!(
12364            handshake.capabilities,
12365            vec![Capability {
12366                name: "ls-refs".into(),
12367                value: Some("unborn".into()),
12368            }]
12369        );
12370        assert_eq!(input, b"next-session");
12371    }
12372
12373    #[test]
12374    fn protocol_v2_advertisement_writes_stream() {
12375        let handshake = TransportHandshake {
12376            protocol: ProtocolVersion::V2,
12377            capabilities: vec![
12378                Capability {
12379                    name: "agent".into(),
12380                    value: Some("sley/0".into()),
12381                },
12382                Capability {
12383                    name: "fetch".into(),
12384                    value: Some("shallow filter".into()),
12385                },
12386            ],
12387        };
12388        let mut encoded = Vec::new();
12389        write_protocol_v2_advertisement(&mut encoded, &handshake)
12390            .expect("test operation should succeed");
12391        let mut input = encoded.as_slice();
12392        assert_eq!(
12393            read_protocol_v2_advertisement(&mut input).expect("test operation should succeed"),
12394            handshake
12395        );
12396        assert!(input.is_empty());
12397        assert!(
12398            encode_protocol_v2_advertisement(&TransportHandshake {
12399                protocol: ProtocolVersion::V1,
12400                capabilities: Vec::new(),
12401            })
12402            .is_err()
12403        );
12404    }
12405
12406    #[test]
12407    fn protocol_v2_advertisement_rejects_malformed_sequences() {
12408        assert!(parse_protocol_v2_advertisement(&[]).is_err());
12409        assert!(
12410            parse_protocol_v2_advertisement(&[
12411                PktLineFrame::Data(b"version 1\n".to_vec()),
12412                PktLineFrame::Flush,
12413            ])
12414            .is_err()
12415        );
12416        assert!(
12417            parse_protocol_v2_advertisement(&[PktLineFrame::Data(b"version 2\n".to_vec())])
12418                .is_err()
12419        );
12420        assert!(
12421            parse_protocol_v2_advertisement(&[
12422                PktLineFrame::Data(b"version 2\n".to_vec()),
12423                PktLineFrame::Delimiter,
12424            ])
12425            .is_err()
12426        );
12427        assert!(
12428            parse_protocol_v2_advertisement(&[
12429                PktLineFrame::Data(b"version 2\n".to_vec()),
12430                PktLineFrame::Data(b"fetch=\n".to_vec()),
12431                PktLineFrame::Flush,
12432            ])
12433            .is_err()
12434        );
12435    }
12436
12437    #[test]
12438    fn protocol_v2_command_request_parses_and_encodes_sections() {
12439        let frames = parse_pkt_line_stream(
12440            b"0014command=ls-refs\n0011agent=sley/0\n0017object-format=sha1\n00010009peel\n000csymrefs\n001bref-prefix refs/heads/\n0000",
12441        )
12442        .expect("test operation should succeed");
12443        let request =
12444            parse_protocol_v2_command_request(&frames).expect("test operation should succeed");
12445        assert_eq!(
12446            request,
12447            ProtocolV2CommandRequest {
12448                command: "ls-refs".into(),
12449                capabilities: vec![
12450                    Capability {
12451                        name: "agent".into(),
12452                        value: Some("sley/0".into()),
12453                    },
12454                    Capability {
12455                        name: "object-format".into(),
12456                        value: Some("sha1".into()),
12457                    },
12458                ],
12459                arguments: vec![
12460                    b"peel".to_vec(),
12461                    b"symrefs".to_vec(),
12462                    b"ref-prefix refs/heads/".to_vec(),
12463                ],
12464            }
12465        );
12466        assert_eq!(
12467            encode_protocol_v2_command_request(&request).expect("test operation should succeed"),
12468            frames
12469        );
12470    }
12471
12472    #[test]
12473    fn protocol_v2_command_request_allows_no_argument_section() {
12474        let frames = parse_pkt_line_stream(b"0012command=fetch\n0000")
12475            .expect("test operation should succeed");
12476        let request =
12477            parse_protocol_v2_command_request(&frames).expect("test operation should succeed");
12478        assert_eq!(
12479            request,
12480            ProtocolV2CommandRequest {
12481                command: "fetch".into(),
12482                capabilities: Vec::new(),
12483                arguments: Vec::new(),
12484            }
12485        );
12486        assert_eq!(
12487            encode_protocol_v2_command_request(&request).expect("test operation should succeed"),
12488            frames
12489        );
12490    }
12491
12492    #[test]
12493    fn protocol_v2_request_parses_commands_and_empty_done() {
12494        let frames = parse_pkt_line_stream(b"0012command=fetch\n0000")
12495            .expect("test operation should succeed");
12496        let command = ProtocolV2CommandRequest {
12497            command: "fetch".into(),
12498            capabilities: Vec::new(),
12499            arguments: Vec::new(),
12500        };
12501        assert_eq!(
12502            parse_protocol_v2_request(&frames).expect("test operation should succeed"),
12503            ProtocolV2Request::Command(command.clone())
12504        );
12505        assert_eq!(
12506            encode_protocol_v2_request(&ProtocolV2Request::Command(command))
12507                .expect("test operation should succeed"),
12508            frames
12509        );
12510
12511        assert_eq!(
12512            parse_protocol_v2_request(&[PktLineFrame::Flush])
12513                .expect("test operation should succeed"),
12514            ProtocolV2Request::Done
12515        );
12516        assert_eq!(
12517            encode_protocol_v2_request(&ProtocolV2Request::Done)
12518                .expect("test operation should succeed"),
12519            vec![PktLineFrame::Flush]
12520        );
12521    }
12522
12523    #[test]
12524    fn protocol_v2_request_streams_empty_done() {
12525        let mut encoded = Vec::new();
12526        write_protocol_v2_request(&mut encoded, &ProtocolV2Request::Done)
12527            .expect("test operation should succeed");
12528        encoded.extend_from_slice(b"tail");
12529
12530        let mut input = encoded.as_slice();
12531        assert_eq!(
12532            read_protocol_v2_request(&mut input).expect("test operation should succeed"),
12533            ProtocolV2Request::Done
12534        );
12535        assert_eq!(input, b"tail");
12536        let mut command_input = encoded.as_slice();
12537        assert!(read_protocol_v2_command_request(&mut command_input).is_err());
12538    }
12539
12540    #[test]
12541    fn protocol_v2_command_request_streams_round_trip() {
12542        let request = ProtocolV2CommandRequest {
12543            command: "ls-refs".into(),
12544            capabilities: vec![Capability {
12545                name: "agent".into(),
12546                value: Some("sley/0".into()),
12547            }],
12548            arguments: vec![b"peel".to_vec(), b"symrefs".to_vec()],
12549        };
12550        let mut encoded = Vec::new();
12551        write_protocol_v2_command_request(&mut encoded, &request)
12552            .expect("test operation should succeed");
12553        encoded.extend_from_slice(b"tail");
12554
12555        let mut input = encoded.as_slice();
12556        assert_eq!(
12557            read_protocol_v2_command_request(&mut input).expect("test operation should succeed"),
12558            request
12559        );
12560        assert_eq!(input, b"tail");
12561    }
12562
12563    #[test]
12564    fn protocol_v2_command_request_rejects_malformed_sequences() {
12565        assert!(parse_protocol_v2_command_request(&[]).is_err());
12566        assert!(
12567            parse_protocol_v2_command_request(&[
12568                PktLineFrame::Data(b"agent=sley/0\n".to_vec()),
12569                PktLineFrame::Flush,
12570            ])
12571            .is_err()
12572        );
12573        assert!(
12574            parse_protocol_v2_command_request(&[
12575                PktLineFrame::Data(b"command=ls-refs\n".to_vec()),
12576                PktLineFrame::Delimiter,
12577                PktLineFrame::Delimiter,
12578                PktLineFrame::Flush,
12579            ])
12580            .is_err()
12581        );
12582        assert!(
12583            parse_protocol_v2_command_request(&[
12584                PktLineFrame::Data(b"command=ls-refs\n".to_vec()),
12585                PktLineFrame::Delimiter,
12586                PktLineFrame::Data(b"\n".to_vec()),
12587                PktLineFrame::Flush,
12588            ])
12589            .is_err()
12590        );
12591        assert!(
12592            encode_protocol_v2_command_request(&ProtocolV2CommandRequest {
12593                command: "bad command".into(),
12594                capabilities: Vec::new(),
12595                arguments: Vec::new(),
12596            })
12597            .is_err()
12598        );
12599    }
12600
12601    #[test]
12602    fn protocol_v2_ls_refs_request_parses_and_encodes_arguments() {
12603        let command = ProtocolV2CommandRequest {
12604            command: "ls-refs".into(),
12605            capabilities: Vec::new(),
12606            arguments: vec![
12607                b"peel".to_vec(),
12608                b"symrefs".to_vec(),
12609                b"unborn".to_vec(),
12610                b"ref-prefix HEAD".to_vec(),
12611                b"ref-prefix refs/heads/".to_vec(),
12612            ],
12613        };
12614        let request = ProtocolV2LsRefsRequest::from_command_request(&command)
12615            .expect("test operation should succeed");
12616        assert_eq!(
12617            request,
12618            ProtocolV2LsRefsRequest {
12619                peel: true,
12620                symrefs: true,
12621                unborn: true,
12622                ref_prefixes: vec!["HEAD".into(), "refs/heads/".into()],
12623            }
12624        );
12625        assert_eq!(
12626            request
12627                .to_command_request()
12628                .expect("test operation should succeed"),
12629            command
12630        );
12631        assert!(
12632            ProtocolV2LsRefsRequest::from_command_request(&ProtocolV2CommandRequest {
12633                command: "fetch".into(),
12634                capabilities: Vec::new(),
12635                arguments: Vec::new(),
12636            })
12637            .is_err()
12638        );
12639        assert!(
12640            ProtocolV2LsRefsRequest::from_command_request(&ProtocolV2CommandRequest {
12641                command: "ls-refs".into(),
12642                capabilities: Vec::new(),
12643                arguments: vec![b"ref-prefix ".to_vec()],
12644            })
12645            .is_err()
12646        );
12647    }
12648
12649    #[test]
12650    fn protocol_v2_ls_refs_request_streams_round_trip() {
12651        let request = ProtocolV2LsRefsRequest {
12652            peel: true,
12653            symrefs: true,
12654            unborn: false,
12655            ref_prefixes: vec!["HEAD".into(), "refs/tags/".into()],
12656        };
12657        let mut encoded = Vec::new();
12658        write_protocol_v2_ls_refs_request(&mut encoded, &request)
12659            .expect("test operation should succeed");
12660        encoded.extend_from_slice(b"tail");
12661
12662        let mut input = encoded.as_slice();
12663        assert_eq!(
12664            read_protocol_v2_ls_refs_request(&mut input).expect("test operation should succeed"),
12665            request
12666        );
12667        assert_eq!(input, b"tail");
12668    }
12669
12670    #[test]
12671    fn protocol_v2_ls_refs_response_parses_and_encodes_records() {
12672        let oid = ObjectId::from_hex(
12673            ObjectFormat::Sha1,
12674            "1111111111111111111111111111111111111111",
12675        )
12676        .expect("test operation should succeed");
12677        let peeled = ObjectId::from_hex(
12678            ObjectFormat::Sha1,
12679            "2222222222222222222222222222222222222222",
12680        )
12681        .expect("test operation should succeed");
12682        let frames = vec![
12683            PktLineFrame::Data(
12684                b"1111111111111111111111111111111111111111 refs/tags/v1 peeled:2222222222222222222222222222222222222222 symref-target:refs/heads/main custom\n"
12685                    .to_vec(),
12686            ),
12687            PktLineFrame::Data(b"unborn HEAD symref-target:refs/heads/main\n".to_vec()),
12688            PktLineFrame::Flush,
12689        ];
12690        let records = parse_protocol_v2_ls_refs_response(ObjectFormat::Sha1, &frames)
12691            .expect("test operation should succeed");
12692        assert_eq!(
12693            records,
12694            vec![
12695                ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
12696                    oid,
12697                    name: "refs/tags/v1".into(),
12698                    peeled: Some(peeled),
12699                    symref_target: Some("refs/heads/main".into()),
12700                    attributes: vec!["custom".into()],
12701                }),
12702                ProtocolV2LsRefsRecord::Unborn {
12703                    name: "HEAD".into(),
12704                    symref_target: Some("refs/heads/main".into()),
12705                    attributes: Vec::new(),
12706                },
12707            ]
12708        );
12709        assert_eq!(
12710            encode_protocol_v2_ls_refs_response(&records).expect("test operation should succeed"),
12711            frames
12712        );
12713    }
12714
12715    #[test]
12716    fn protocol_v2_ls_refs_response_streams_round_trip() {
12717        let oid = ObjectId::from_hex(
12718            ObjectFormat::Sha1,
12719            "1111111111111111111111111111111111111111",
12720        )
12721        .expect("test operation should succeed");
12722        let records = vec![ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
12723            oid,
12724            name: "refs/heads/main".into(),
12725            peeled: None,
12726            symref_target: Some("refs/heads/trunk".into()),
12727            attributes: vec!["custom".into()],
12728        })];
12729        let mut encoded = Vec::new();
12730        write_protocol_v2_ls_refs_response(&mut encoded, &records)
12731            .expect("test operation should succeed");
12732        encoded.extend_from_slice(b"tail");
12733
12734        let mut input = encoded.as_slice();
12735        assert_eq!(
12736            read_protocol_v2_ls_refs_response(ObjectFormat::Sha1, &mut input)
12737                .expect("test operation should succeed"),
12738            records
12739        );
12740        assert_eq!(input, b"tail");
12741    }
12742
12743    #[test]
12744    fn protocol_v2_ls_refs_response_reads_stateless_response_end() {
12745        let oid = ObjectId::from_hex(
12746            ObjectFormat::Sha1,
12747            "1111111111111111111111111111111111111111",
12748        )
12749        .expect("test operation should succeed");
12750        let records = vec![ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
12751            oid,
12752            name: "refs/heads/main".into(),
12753            peeled: None,
12754            symref_target: None,
12755            attributes: Vec::new(),
12756        })];
12757        let mut encoded = Vec::new();
12758        write_protocol_v2_ls_refs_response_with_response_end(&mut encoded, &records)
12759            .expect("test operation should succeed");
12760        encoded.extend_from_slice(b"tail");
12761
12762        let mut input = encoded.as_slice();
12763        assert_eq!(
12764            read_protocol_v2_ls_refs_response_until_response_end(ObjectFormat::Sha1, &mut input)
12765                .expect("test operation should succeed"),
12766            records
12767        );
12768        assert_eq!(input, b"tail");
12769        assert!(
12770            parse_protocol_v2_ls_refs_response(
12771                ObjectFormat::Sha1,
12772                &[
12773                    PktLineFrame::Data(
12774                        b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec()
12775                    ),
12776                    PktLineFrame::ResponseEnd
12777                ],
12778            )
12779            .is_err()
12780        );
12781    }
12782
12783    #[test]
12784    fn protocol_v2_ls_refs_exchange_writes_request_and_reads_response() {
12785        let oid = ObjectId::from_hex(
12786            ObjectFormat::Sha1,
12787            "1111111111111111111111111111111111111111",
12788        )
12789        .expect("test operation should succeed");
12790        let request = ProtocolV2LsRefsRequest {
12791            peel: true,
12792            symrefs: true,
12793            unborn: false,
12794            ref_prefixes: vec!["refs/heads/".into()],
12795        };
12796        let records = vec![ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
12797            oid,
12798            name: "refs/heads/main".into(),
12799            peeled: None,
12800            symref_target: None,
12801            attributes: Vec::new(),
12802        })];
12803        let mut response = Vec::new();
12804        write_protocol_v2_ls_refs_response(&mut response, &records)
12805            .expect("test operation should succeed");
12806
12807        let mut input = response.as_slice();
12808        let mut output = Vec::new();
12809        assert_eq!(
12810            exchange_protocol_v2_ls_refs(ObjectFormat::Sha1, &mut input, &mut output, &request)
12811                .expect("test operation should succeed"),
12812            records
12813        );
12814        assert!(input.is_empty());
12815        let mut output_read = output.as_slice();
12816        assert_eq!(
12817            read_protocol_v2_ls_refs_request(&mut output_read)
12818                .expect("test operation should succeed"),
12819            request
12820        );
12821    }
12822
12823    #[test]
12824    fn protocol_v2_ls_refs_response_rejects_malformed_records() {
12825        assert!(
12826            parse_protocol_v2_ls_refs_response(
12827                ObjectFormat::Sha1,
12828                &[PktLineFrame::Data(
12829                    b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec()
12830                )],
12831            )
12832            .is_err()
12833        );
12834        assert!(
12835            parse_protocol_v2_ls_refs_response(
12836                ObjectFormat::Sha1,
12837                &[
12838                    PktLineFrame::Data(
12839                        b"1111111111111111111111111111111111111111 refs/heads/main peeled:2222222222222222222222222222222222222222 peeled:3333333333333333333333333333333333333333\n"
12840                            .to_vec()
12841                    ),
12842                    PktLineFrame::Flush,
12843                ],
12844            )
12845            .is_err()
12846        );
12847        assert!(
12848            parse_protocol_v2_ls_refs_response(
12849                ObjectFormat::Sha1,
12850                &[
12851                    PktLineFrame::Data(
12852                        b"unborn HEAD peeled:2222222222222222222222222222222222222222\n".to_vec()
12853                    ),
12854                    PktLineFrame::Flush,
12855                ],
12856            )
12857            .is_err()
12858        );
12859        assert!(
12860            encode_protocol_v2_ls_refs_response(&[ProtocolV2LsRefsRecord::Ref(
12861                ProtocolV2LsRefsRef {
12862                    oid: ObjectId::from_hex(
12863                        ObjectFormat::Sha1,
12864                        "1111111111111111111111111111111111111111",
12865                    )
12866                    .expect("test operation should succeed"),
12867                    name: "refs/heads/main".into(),
12868                    peeled: None,
12869                    symref_target: None,
12870                    attributes: vec!["peeled:2222222222222222222222222222222222222222".into()],
12871                }
12872            )])
12873            .is_err()
12874        );
12875    }
12876
12877    #[test]
12878    fn protocol_v2_fetch_request_parses_and_encodes_arguments() {
12879        let want = ObjectId::from_hex(
12880            ObjectFormat::Sha1,
12881            "1111111111111111111111111111111111111111",
12882        )
12883        .expect("test operation should succeed");
12884        let have = ObjectId::from_hex(
12885            ObjectFormat::Sha1,
12886            "2222222222222222222222222222222222222222",
12887        )
12888        .expect("test operation should succeed");
12889        let shallow = ObjectId::from_hex(
12890            ObjectFormat::Sha1,
12891            "3333333333333333333333333333333333333333",
12892        )
12893        .expect("test operation should succeed");
12894        let command = ProtocolV2CommandRequest {
12895            command: "fetch".into(),
12896            capabilities: Vec::new(),
12897            arguments: vec![
12898                b"want 1111111111111111111111111111111111111111".to_vec(),
12899                b"want-ref refs/heads/main".to_vec(),
12900                b"have 2222222222222222222222222222222222222222".to_vec(),
12901                b"shallow 3333333333333333333333333333333333333333".to_vec(),
12902                b"deepen 10".to_vec(),
12903                b"deepen-since 123456789".to_vec(),
12904                b"deepen-not refs/tags/v1".to_vec(),
12905                b"deepen-relative".to_vec(),
12906                b"filter blob:none".to_vec(),
12907                b"packfile-uris http,https".to_vec(),
12908                b"thin-pack".to_vec(),
12909                b"no-progress".to_vec(),
12910                b"include-tag".to_vec(),
12911                b"ofs-delta".to_vec(),
12912                b"sideband-all".to_vec(),
12913                b"wait-for-done".to_vec(),
12914                b"done".to_vec(),
12915            ],
12916        };
12917        let request = ProtocolV2FetchRequest::from_command_request(ObjectFormat::Sha1, &command)
12918            .expect("test operation should succeed");
12919        assert_eq!(
12920            request,
12921            ProtocolV2FetchRequest {
12922                wants: vec![want],
12923                want_refs: vec!["refs/heads/main".into()],
12924                haves: vec![have],
12925                shallow: vec![shallow],
12926                deepen: Some(10),
12927                deepen_since: Some(123456789),
12928                deepen_not: vec!["refs/tags/v1".into()],
12929                deepen_relative: true,
12930                filter: Some("blob:none".into()),
12931                packfile_uris: Some("http,https".into()),
12932                thin_pack: true,
12933                no_progress: true,
12934                include_tag: true,
12935                ofs_delta: true,
12936                sideband_all: true,
12937                wait_for_done: true,
12938                done: true,
12939            }
12940        );
12941        assert_eq!(
12942            request
12943                .to_command_request()
12944                .expect("test operation should succeed"),
12945            command
12946        );
12947    }
12948
12949    #[test]
12950    fn protocol_v2_fetch_request_rejects_malformed_arguments() {
12951        assert!(
12952            ProtocolV2FetchRequest::from_command_request(
12953                ObjectFormat::Sha1,
12954                &ProtocolV2CommandRequest {
12955                    command: "ls-refs".into(),
12956                    capabilities: Vec::new(),
12957                    arguments: Vec::new(),
12958                },
12959            )
12960            .is_err()
12961        );
12962        assert!(
12963            ProtocolV2FetchRequest::from_command_request(
12964                ObjectFormat::Sha1,
12965                &ProtocolV2CommandRequest {
12966                    command: "fetch".into(),
12967                    capabilities: Vec::new(),
12968                    arguments: vec![b"want not-an-oid".to_vec()],
12969                },
12970            )
12971            .is_err()
12972        );
12973        assert!(
12974            ProtocolV2FetchRequest::from_command_request(
12975                ObjectFormat::Sha1,
12976                &ProtocolV2CommandRequest {
12977                    command: "fetch".into(),
12978                    capabilities: Vec::new(),
12979                    arguments: vec![b"deepen 0".to_vec()],
12980                },
12981            )
12982            .is_err()
12983        );
12984        assert!(
12985            ProtocolV2FetchRequest::from_command_request(
12986                ObjectFormat::Sha1,
12987                &ProtocolV2CommandRequest {
12988                    command: "fetch".into(),
12989                    capabilities: Vec::new(),
12990                    arguments: vec![b"filter blob:none".to_vec(), b"filter tree:0".to_vec()],
12991                },
12992            )
12993            .is_err()
12994        );
12995        assert!(
12996            ProtocolV2FetchRequest {
12997                deepen: Some(0),
12998                ..ProtocolV2FetchRequest::default()
12999            }
13000            .to_command_request()
13001            .is_err()
13002        );
13003    }
13004
13005    #[test]
13006    fn protocol_v2_fetch_request_streams_round_trip() {
13007        let want = ObjectId::from_hex(
13008            ObjectFormat::Sha1,
13009            "1111111111111111111111111111111111111111",
13010        )
13011        .expect("test operation should succeed");
13012        let have = ObjectId::from_hex(
13013            ObjectFormat::Sha1,
13014            "2222222222222222222222222222222222222222",
13015        )
13016        .expect("test operation should succeed");
13017        let request = ProtocolV2FetchRequest {
13018            wants: vec![want],
13019            haves: vec![have],
13020            deepen: Some(5),
13021            filter: Some("blob:none".into()),
13022            thin_pack: true,
13023            done: true,
13024            ..ProtocolV2FetchRequest::default()
13025        };
13026        let mut encoded = Vec::new();
13027        write_protocol_v2_fetch_request(&mut encoded, &request)
13028            .expect("test operation should succeed");
13029        encoded.extend_from_slice(b"tail");
13030
13031        let mut input = encoded.as_slice();
13032        assert_eq!(
13033            read_protocol_v2_fetch_request(ObjectFormat::Sha1, &mut input)
13034                .expect("test operation should succeed"),
13035            request
13036        );
13037        assert_eq!(input, b"tail");
13038    }
13039
13040    #[test]
13041    fn protocol_v2_fetch_response_parses_and_encodes_sections() {
13042        let ack = ObjectId::from_hex(
13043            ObjectFormat::Sha1,
13044            "1111111111111111111111111111111111111111",
13045        )
13046        .expect("test operation should succeed");
13047        let shallow = ObjectId::from_hex(
13048            ObjectFormat::Sha1,
13049            "2222222222222222222222222222222222222222",
13050        )
13051        .expect("test operation should succeed");
13052        let wanted = ObjectId::from_hex(
13053            ObjectFormat::Sha1,
13054            "3333333333333333333333333333333333333333",
13055        )
13056        .expect("test operation should succeed");
13057        let pack_hash = ObjectId::from_hex(
13058            ObjectFormat::Sha1,
13059            "4444444444444444444444444444444444444444",
13060        )
13061        .expect("test operation should succeed");
13062        let frames = vec![
13063            PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13064            PktLineFrame::Data(b"ACK 1111111111111111111111111111111111111111\n".to_vec()),
13065            PktLineFrame::Data(b"ready\n".to_vec()),
13066            PktLineFrame::Delimiter,
13067            PktLineFrame::Data(b"shallow-info\n".to_vec()),
13068            PktLineFrame::Data(b"shallow 2222222222222222222222222222222222222222\n".to_vec()),
13069            PktLineFrame::Delimiter,
13070            PktLineFrame::Data(b"wanted-refs\n".to_vec()),
13071            PktLineFrame::Data(
13072                b"3333333333333333333333333333333333333333 refs/heads/main\n".to_vec(),
13073            ),
13074            PktLineFrame::Delimiter,
13075            PktLineFrame::Data(b"packfile-uris\n".to_vec()),
13076            PktLineFrame::Data(
13077                b"4444444444444444444444444444444444444444 https://example.invalid/pack-a.pack\n"
13078                    .to_vec(),
13079            ),
13080            PktLineFrame::Delimiter,
13081            PktLineFrame::Data(b"packfile\n".to_vec()),
13082            PktLineFrame::Data(b"\x01PACK bytes".to_vec()),
13083            PktLineFrame::Flush,
13084        ];
13085        let sections = parse_protocol_v2_fetch_response(ObjectFormat::Sha1, &frames)
13086            .expect("test operation should succeed");
13087        assert_eq!(
13088            sections,
13089            vec![
13090                ProtocolV2FetchResponseSection::Acknowledgments(vec![
13091                    ProtocolV2FetchAcknowledgment::Ack(ack),
13092                    ProtocolV2FetchAcknowledgment::Ready,
13093                ]),
13094                ProtocolV2FetchResponseSection::ShallowInfo(vec![
13095                    ProtocolV2FetchShallowInfo::Shallow(shallow)
13096                ]),
13097                ProtocolV2FetchResponseSection::WantedRefs(vec![ProtocolV2FetchWantedRef {
13098                    oid: wanted,
13099                    name: "refs/heads/main".into(),
13100                }]),
13101                ProtocolV2FetchResponseSection::PackfileUris(vec![ProtocolV2FetchPackfileUri {
13102                    pack_hash,
13103                    uri: "https://example.invalid/pack-a.pack".into(),
13104                }]),
13105                ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK bytes".to_vec()]),
13106            ]
13107        );
13108        assert_eq!(
13109            encode_protocol_v2_fetch_response(&sections).expect("test operation should succeed"),
13110            frames
13111        );
13112    }
13113
13114    #[test]
13115    fn protocol_v2_fetch_response_preserves_unknown_sections() {
13116        let frames = vec![
13117            PktLineFrame::Data(b"server-feature\n".to_vec()),
13118            PktLineFrame::Data(b"opaque line\n".to_vec()),
13119            PktLineFrame::Flush,
13120        ];
13121        let sections = parse_protocol_v2_fetch_response(ObjectFormat::Sha1, &frames)
13122            .expect("test operation should succeed");
13123        assert_eq!(
13124            sections,
13125            vec![ProtocolV2FetchResponseSection::Unknown {
13126                name: "server-feature".into(),
13127                lines: vec![b"opaque line\n".to_vec()],
13128            }]
13129        );
13130        assert_eq!(
13131            encode_protocol_v2_fetch_response(&sections).expect("test operation should succeed"),
13132            frames
13133        );
13134    }
13135
13136    #[test]
13137    fn protocol_v2_fetch_response_streams_round_trip() {
13138        let ack = ObjectId::from_hex(
13139            ObjectFormat::Sha1,
13140            "1111111111111111111111111111111111111111",
13141        )
13142        .expect("test operation should succeed");
13143        let sections = vec![
13144            ProtocolV2FetchResponseSection::Acknowledgments(vec![
13145                ProtocolV2FetchAcknowledgment::Ack(ack),
13146                ProtocolV2FetchAcknowledgment::Ready,
13147            ]),
13148            ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK bytes".to_vec()]),
13149        ];
13150        let mut encoded = Vec::new();
13151        write_protocol_v2_fetch_response(&mut encoded, &sections)
13152            .expect("test operation should succeed");
13153        encoded.extend_from_slice(b"tail");
13154
13155        let mut input = encoded.as_slice();
13156        assert_eq!(
13157            read_protocol_v2_fetch_response(ObjectFormat::Sha1, &mut input)
13158                .expect("test operation should succeed"),
13159            sections
13160        );
13161        assert_eq!(input, b"tail");
13162    }
13163
13164    #[test]
13165    fn protocol_v2_fetch_sideband_all_response_parses_sections_and_progress() {
13166        let frames = vec![
13167            PktLineFrame::Data(
13168                encode_sideband_packet(&SideBandPacket {
13169                    channel: SideBandChannel::Data,
13170                    data: b"acknowledgments\n".to_vec(),
13171                })
13172                .expect("test operation should succeed"),
13173            ),
13174            PktLineFrame::Data(
13175                encode_sideband_packet(&SideBandPacket {
13176                    channel: SideBandChannel::Data,
13177                    data: b"NAK\n".to_vec(),
13178                })
13179                .expect("test operation should succeed"),
13180            ),
13181            PktLineFrame::Data(
13182                encode_sideband_packet(&SideBandPacket {
13183                    channel: SideBandChannel::Progress,
13184                    data: b"keepalive\n".to_vec(),
13185                })
13186                .expect("test operation should succeed"),
13187            ),
13188            PktLineFrame::Delimiter,
13189            PktLineFrame::Data(
13190                encode_sideband_packet(&SideBandPacket {
13191                    channel: SideBandChannel::Data,
13192                    data: b"packfile\n".to_vec(),
13193                })
13194                .expect("test operation should succeed"),
13195            ),
13196            PktLineFrame::Data(b"\x01PACK".to_vec()),
13197            PktLineFrame::Data(b"\x02counting objects\n".to_vec()),
13198            PktLineFrame::Flush,
13199        ];
13200
13201        let response = parse_protocol_v2_fetch_sideband_all_response(ObjectFormat::Sha1, &frames)
13202            .expect("test operation should succeed");
13203        assert_eq!(
13204            response,
13205            ProtocolV2FetchSidebandAllResponse {
13206                sections: vec![
13207                    ProtocolV2FetchResponseSection::Acknowledgments(vec![
13208                        ProtocolV2FetchAcknowledgment::Nak
13209                    ]),
13210                    ProtocolV2FetchResponseSection::Packfile(vec![
13211                        b"\x01PACK".to_vec(),
13212                        b"\x02counting objects\n".to_vec(),
13213                    ]),
13214                ],
13215                progress: vec![b"keepalive\n".to_vec()],
13216            }
13217        );
13218        assert_eq!(
13219            demux_protocol_v2_fetch_packfile(&response.sections)
13220                .expect("test operation should succeed"),
13221            Some(SideBandDemux {
13222                data: b"PACK".to_vec(),
13223                progress: vec![b"counting objects\n".to_vec()],
13224            })
13225        );
13226    }
13227
13228    #[test]
13229    fn protocol_v2_fetch_sideband_all_response_streams_round_trip() {
13230        let sections = vec![
13231            ProtocolV2FetchResponseSection::Acknowledgments(vec![
13232                ProtocolV2FetchAcknowledgment::Nak,
13233            ]),
13234            ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK bytes".to_vec()]),
13235        ];
13236        let mut encoded = Vec::new();
13237        write_protocol_v2_fetch_sideband_all_response(&mut encoded, &sections)
13238            .expect("test operation should succeed");
13239        encoded.extend_from_slice(b"tail");
13240
13241        let mut input = encoded.as_slice();
13242        assert_eq!(
13243            read_protocol_v2_fetch_sideband_all_response(ObjectFormat::Sha1, &mut input)
13244                .expect("test operation should succeed"),
13245            ProtocolV2FetchSidebandAllResponse {
13246                sections: sections.clone(),
13247                progress: Vec::new(),
13248            }
13249        );
13250        assert_eq!(input, b"tail");
13251
13252        let mut encoded = Vec::new();
13253        write_protocol_v2_fetch_sideband_all_response_with_response_end(&mut encoded, &sections)
13254            .expect("test operation should succeed");
13255        encoded.extend_from_slice(b"tail");
13256
13257        let mut input = encoded.as_slice();
13258        assert_eq!(
13259            read_protocol_v2_fetch_sideband_all_response_until_response_end(
13260                ObjectFormat::Sha1,
13261                &mut input,
13262            )
13263            .expect("test operation should succeed")
13264            .sections,
13265            sections
13266        );
13267        assert_eq!(input, b"tail");
13268    }
13269
13270    #[test]
13271    fn protocol_v2_fetch_sideband_all_response_rejects_malformed_sideband() {
13272        assert!(
13273            parse_protocol_v2_fetch_sideband_all_response(
13274                ObjectFormat::Sha1,
13275                &[
13276                    PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13277                    PktLineFrame::Flush,
13278                ],
13279            )
13280            .is_err()
13281        );
13282        assert!(
13283            parse_protocol_v2_fetch_sideband_all_response(
13284                ObjectFormat::Sha1,
13285                &[
13286                    PktLineFrame::Data(
13287                        encode_sideband_packet(&SideBandPacket {
13288                            channel: SideBandChannel::Fatal,
13289                            data: b"remote died\n".to_vec(),
13290                        })
13291                        .expect("test operation should succeed"),
13292                    ),
13293                    PktLineFrame::Flush,
13294                ],
13295            )
13296            .is_err()
13297        );
13298    }
13299
13300    #[test]
13301    fn protocol_v2_object_info_response_parses_and_encodes_size_records() {
13302        let oid = ObjectId::from_hex(
13303            ObjectFormat::Sha1,
13304            "1111111111111111111111111111111111111111",
13305        )
13306        .expect("test operation should succeed");
13307        let frames = vec![
13308            PktLineFrame::Data(b"size\n".to_vec()),
13309            PktLineFrame::Data(b"1111111111111111111111111111111111111111 12345\n".to_vec()),
13310            PktLineFrame::Flush,
13311        ];
13312        let response = parse_protocol_v2_object_info_response(ObjectFormat::Sha1, &frames)
13313            .expect("test operation should succeed");
13314        assert_eq!(
13315            response,
13316            ProtocolV2ObjectInfoResponse {
13317                size: true,
13318                records: vec![ProtocolV2ObjectInfoRecord { oid, size: 12345 }],
13319            }
13320        );
13321        assert_eq!(
13322            encode_protocol_v2_object_info_response(&response)
13323                .expect("test operation should succeed"),
13324            frames
13325        );
13326    }
13327
13328    #[test]
13329    fn protocol_v2_object_info_response_streams_and_exchanges() {
13330        let request = ProtocolV2ObjectInfoRequest {
13331            size: true,
13332            oids: vec![
13333                ObjectId::from_hex(
13334                    ObjectFormat::Sha1,
13335                    "1111111111111111111111111111111111111111",
13336                )
13337                .expect("test operation should succeed"),
13338            ],
13339        };
13340        let response = ProtocolV2ObjectInfoResponse {
13341            size: true,
13342            records: vec![ProtocolV2ObjectInfoRecord {
13343                oid: request.oids[0].clone(),
13344                size: 7,
13345            }],
13346        };
13347
13348        let mut encoded = Vec::new();
13349        write_protocol_v2_object_info_response(&mut encoded, &response)
13350            .expect("test operation should succeed");
13351        encoded.extend_from_slice(b"tail");
13352        let mut input = encoded.as_slice();
13353        assert_eq!(
13354            read_protocol_v2_object_info_response(ObjectFormat::Sha1, &mut input)
13355                .expect("test operation should succeed"),
13356            response
13357        );
13358        assert_eq!(input, b"tail");
13359
13360        let mut response_bytes = Vec::new();
13361        write_protocol_v2_object_info_response(&mut response_bytes, &response)
13362            .expect("test operation should succeed");
13363        let mut input = response_bytes.as_slice();
13364        let mut output = Vec::new();
13365        assert_eq!(
13366            exchange_protocol_v2_object_info(
13367                ObjectFormat::Sha1,
13368                &mut input,
13369                &mut output,
13370                &request,
13371            )
13372            .expect("test operation should succeed"),
13373            response
13374        );
13375        assert!(input.is_empty());
13376        let mut output_read = output.as_slice();
13377        assert_eq!(
13378            read_protocol_v2_object_info_request(ObjectFormat::Sha1, &mut output_read)
13379                .expect("test operation should succeed"),
13380            request
13381        );
13382    }
13383
13384    #[test]
13385    fn protocol_v2_object_info_response_rejects_malformed_records() {
13386        assert!(parse_protocol_v2_object_info_response(ObjectFormat::Sha1, &[]).is_err());
13387        assert!(
13388            parse_protocol_v2_object_info_response(
13389                ObjectFormat::Sha1,
13390                &[PktLineFrame::Data(b"size\n".to_vec())],
13391            )
13392            .is_err()
13393        );
13394        assert!(
13395            parse_protocol_v2_object_info_response(
13396                ObjectFormat::Sha1,
13397                &[PktLineFrame::Data(b"type\n".to_vec()), PktLineFrame::Flush,],
13398            )
13399            .is_err()
13400        );
13401        assert!(
13402            parse_protocol_v2_object_info_response(
13403                ObjectFormat::Sha1,
13404                &[
13405                    PktLineFrame::Data(b"size\n".to_vec()),
13406                    PktLineFrame::Data(
13407                        b"1111111111111111111111111111111111111111 not-a-size\n".to_vec()
13408                    ),
13409                    PktLineFrame::Flush,
13410                ],
13411            )
13412            .is_err()
13413        );
13414        assert!(
13415            parse_protocol_v2_object_info_response(
13416                ObjectFormat::Sha1,
13417                &[
13418                    PktLineFrame::Data(b"size\n".to_vec()),
13419                    PktLineFrame::Delimiter,
13420                    PktLineFrame::Flush,
13421                ],
13422            )
13423            .is_err()
13424        );
13425        assert!(
13426            encode_protocol_v2_object_info_response(&ProtocolV2ObjectInfoResponse {
13427                size: false,
13428                records: Vec::new(),
13429            })
13430            .is_err()
13431        );
13432    }
13433
13434    #[test]
13435    fn protocol_v2_fetch_response_reads_stateless_response_end() {
13436        let sections = vec![ProtocolV2FetchResponseSection::Acknowledgments(vec![
13437            ProtocolV2FetchAcknowledgment::Nak,
13438        ])];
13439        let mut encoded = Vec::new();
13440        write_protocol_v2_fetch_response_with_response_end(&mut encoded, &sections)
13441            .expect("test operation should succeed");
13442        encoded.extend_from_slice(b"tail");
13443
13444        let mut input = encoded.as_slice();
13445        assert_eq!(
13446            read_protocol_v2_fetch_response_until_response_end(ObjectFormat::Sha1, &mut input)
13447                .expect("test operation should succeed"),
13448            sections
13449        );
13450        assert_eq!(input, b"tail");
13451        assert!(
13452            parse_protocol_v2_fetch_response(
13453                ObjectFormat::Sha1,
13454                &[
13455                    PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13456                    PktLineFrame::ResponseEnd,
13457                ],
13458            )
13459            .is_err()
13460        );
13461    }
13462
13463    #[test]
13464    fn protocol_v2_fetch_exchange_writes_request_and_reads_response() {
13465        let want = ObjectId::from_hex(
13466            ObjectFormat::Sha1,
13467            "1111111111111111111111111111111111111111",
13468        )
13469        .expect("test operation should succeed");
13470        let request = ProtocolV2FetchRequest {
13471            wants: vec![want],
13472            thin_pack: true,
13473            done: true,
13474            ..ProtocolV2FetchRequest::default()
13475        };
13476        let sections = vec![ProtocolV2FetchResponseSection::Acknowledgments(vec![
13477            ProtocolV2FetchAcknowledgment::Nak,
13478        ])];
13479        let mut response = Vec::new();
13480        write_protocol_v2_fetch_response(&mut response, &sections)
13481            .expect("test operation should succeed");
13482
13483        let mut input = response.as_slice();
13484        let mut output = Vec::new();
13485        assert_eq!(
13486            exchange_protocol_v2_fetch(ObjectFormat::Sha1, &mut input, &mut output, &request)
13487                .expect("test operation should succeed"),
13488            sections
13489        );
13490        assert!(input.is_empty());
13491        let mut output_read = output.as_slice();
13492        assert_eq!(
13493            read_protocol_v2_fetch_request(ObjectFormat::Sha1, &mut output_read)
13494                .expect("test operation should succeed"),
13495            request
13496        );
13497    }
13498
13499    #[test]
13500    fn protocol_v2_fetch_packfile_demuxes_sideband_section() {
13501        let sections = vec![
13502            ProtocolV2FetchResponseSection::Acknowledgments(vec![
13503                ProtocolV2FetchAcknowledgment::Nak,
13504            ]),
13505            ProtocolV2FetchResponseSection::Packfile(vec![
13506                b"\x01PACK".to_vec(),
13507                b"\x02counting objects\n".to_vec(),
13508                b"\x01 bytes".to_vec(),
13509                b"\x02done\n".to_vec(),
13510            ]),
13511        ];
13512
13513        assert_eq!(
13514            demux_protocol_v2_fetch_packfile(&sections).expect("test operation should succeed"),
13515            Some(SideBandDemux {
13516                data: b"PACK bytes".to_vec(),
13517                progress: vec![b"counting objects\n".to_vec(), b"done\n".to_vec()],
13518            })
13519        );
13520        assert_eq!(
13521            demux_protocol_v2_fetch_packfile(&[ProtocolV2FetchResponseSection::Acknowledgments(
13522                vec![ProtocolV2FetchAcknowledgment::Nak],
13523            )])
13524            .expect("test operation should succeed"),
13525            None
13526        );
13527    }
13528
13529    #[test]
13530    fn protocol_v2_fetch_packfile_demux_rejects_duplicate_or_bad_sideband() {
13531        assert!(
13532            demux_protocol_v2_fetch_packfile(&[
13533                ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK".to_vec()]),
13534                ProtocolV2FetchResponseSection::Packfile(vec![b"\x01more".to_vec()]),
13535            ])
13536            .is_err()
13537        );
13538        assert!(
13539            demux_protocol_v2_fetch_packfile(&[ProtocolV2FetchResponseSection::Packfile(vec![
13540                b"\x03remote died\n".to_vec()
13541            ])])
13542            .is_err()
13543        );
13544        assert!(
13545            demux_protocol_v2_fetch_packfile(&[ProtocolV2FetchResponseSection::Packfile(vec![
13546                b"\x04bad".to_vec()
13547            ])])
13548            .is_err()
13549        );
13550    }
13551
13552    #[test]
13553    fn protocol_v2_fetch_response_rejects_malformed_sections() {
13554        assert!(
13555            parse_protocol_v2_fetch_response(
13556                ObjectFormat::Sha1,
13557                &[PktLineFrame::Data(b"acknowledgments\n".to_vec())],
13558            )
13559            .is_err()
13560        );
13561        assert!(
13562            parse_protocol_v2_fetch_response(
13563                ObjectFormat::Sha1,
13564                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
13565            )
13566            .is_err()
13567        );
13568        assert!(
13569            parse_protocol_v2_fetch_response(
13570                ObjectFormat::Sha1,
13571                &[
13572                    PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13573                    PktLineFrame::Data(b"ACK not-an-oid\n".to_vec()),
13574                    PktLineFrame::Flush,
13575                ],
13576            )
13577            .is_err()
13578        );
13579        assert!(
13580            parse_protocol_v2_fetch_response(
13581                ObjectFormat::Sha1,
13582                &[
13583                    PktLineFrame::Data(b"packfile-uris\n".to_vec()),
13584                    PktLineFrame::Data(b"https://example.invalid/pack-a.pack\n".to_vec()),
13585                    PktLineFrame::Flush,
13586                ],
13587            )
13588            .is_err()
13589        );
13590        assert!(
13591            parse_protocol_v2_fetch_response(
13592                ObjectFormat::Sha1,
13593                &[
13594                    PktLineFrame::Data(b"packfile-uris\n".to_vec()),
13595                    PktLineFrame::Data(
13596                        b"not-a-hash https://example.invalid/pack-a.pack\n".to_vec()
13597                    ),
13598                    PktLineFrame::Flush,
13599                ],
13600            )
13601            .is_err()
13602        );
13603        assert!(
13604            encode_protocol_v2_fetch_response(&[ProtocolV2FetchResponseSection::WantedRefs(vec![
13605                ProtocolV2FetchWantedRef {
13606                    oid: ObjectId::from_hex(
13607                        ObjectFormat::Sha1,
13608                        "1111111111111111111111111111111111111111",
13609                    )
13610                    .expect("test operation should succeed"),
13611                    name: "bad ref".into(),
13612                }
13613            ])])
13614            .is_err()
13615        );
13616    }
13617
13618    #[test]
13619    fn protocol_v2_ls_refs_response_bridges_into_ref_advertisement_set() {
13620        let head = ObjectId::from_hex(
13621            ObjectFormat::Sha1,
13622            "1111111111111111111111111111111111111111",
13623        )
13624        .expect("test operation should succeed");
13625        let tag = ObjectId::from_hex(
13626            ObjectFormat::Sha1,
13627            "2222222222222222222222222222222222222222",
13628        )
13629        .expect("test operation should succeed");
13630        let tag_peeled = ObjectId::from_hex(
13631            ObjectFormat::Sha1,
13632            "3333333333333333333333333333333333333333",
13633        )
13634        .expect("test operation should succeed");
13635        let frames = vec![
13636            PktLineFrame::Data(
13637                b"1111111111111111111111111111111111111111 HEAD symref-target:refs/heads/main\n"
13638                    .to_vec(),
13639            ),
13640            PktLineFrame::Data(
13641                b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec(),
13642            ),
13643            PktLineFrame::Data(
13644                b"2222222222222222222222222222222222222222 refs/tags/v1 peeled:3333333333333333333333333333333333333333\n"
13645                    .to_vec(),
13646            ),
13647            PktLineFrame::Flush,
13648        ];
13649
13650        let set = parse_protocol_v2_ls_refs_response_as_ref_advertisement_set(
13651            ObjectFormat::Sha1,
13652            &frames,
13653        )
13654        .expect("test operation should succeed");
13655        assert_eq!(
13656            set,
13657            RefAdvertisementSet {
13658                protocol: ProtocolVersion::V2,
13659                refs: vec![
13660                    RefAdvertisement {
13661                        oid: head.clone(),
13662                        name: "HEAD".into(),
13663                        capabilities: vec![Capability {
13664                            name: "symref".into(),
13665                            value: Some("HEAD:refs/heads/main".into()),
13666                        }],
13667                    },
13668                    RefAdvertisement {
13669                        oid: head,
13670                        name: "refs/heads/main".into(),
13671                        capabilities: Vec::new(),
13672                    },
13673                    RefAdvertisement {
13674                        oid: tag,
13675                        name: "refs/tags/v1".into(),
13676                        capabilities: Vec::new(),
13677                    },
13678                    RefAdvertisement {
13679                        oid: tag_peeled,
13680                        name: "refs/tags/v1^{}".into(),
13681                        capabilities: Vec::new(),
13682                    },
13683                ],
13684                shallow: Vec::new(),
13685            }
13686        );
13687
13688        // The streaming reader path produces the same bridged set.
13689        let mut encoded = Vec::new();
13690        write_pkt_line_frames(&mut encoded, &frames).expect("test operation should succeed");
13691        encoded.extend_from_slice(b"tail");
13692        let mut input = encoded.as_slice();
13693        assert_eq!(
13694            read_protocol_v2_ls_refs_response_as_ref_advertisement_set(
13695                ObjectFormat::Sha1,
13696                &mut input,
13697            )
13698            .expect("test operation should succeed"),
13699            set,
13700        );
13701        assert_eq!(input, b"tail");
13702    }
13703
13704    #[test]
13705    fn protocol_v2_ls_refs_records_bridge_unborn_head_symref_and_empty() {
13706        // An unborn HEAD pointing at an as-yet-uncreated branch carries only a
13707        // symref capability and has no concrete ref to attach it to.
13708        let records = vec![ProtocolV2LsRefsRecord::Unborn {
13709            name: "HEAD".into(),
13710            symref_target: Some("refs/heads/main".into()),
13711            attributes: Vec::new(),
13712        }];
13713        assert!(protocol_v2_ls_refs_records_to_ref_advertisement_set(&records).is_err());
13714
13715        // An empty ls-refs response bridges to an empty v2 set.
13716        assert_eq!(
13717            protocol_v2_ls_refs_records_to_ref_advertisement_set(&[])
13718                .expect("test operation should succeed"),
13719            RefAdvertisementSet {
13720                protocol: ProtocolVersion::V2,
13721                refs: Vec::new(),
13722                shallow: Vec::new(),
13723            }
13724        );
13725
13726        // An unborn HEAD alongside a concrete ref attaches the symref to the
13727        // first ref, matching the v0/v1 advertisement convention.
13728        let main = ObjectId::from_hex(
13729            ObjectFormat::Sha1,
13730            "4444444444444444444444444444444444444444",
13731        )
13732        .expect("test operation should succeed");
13733        let records = vec![
13734            ProtocolV2LsRefsRecord::Unborn {
13735                name: "HEAD".into(),
13736                symref_target: Some("refs/heads/main".into()),
13737                attributes: Vec::new(),
13738            },
13739            ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
13740                oid: main.clone(),
13741                name: "refs/heads/main".into(),
13742                peeled: None,
13743                symref_target: None,
13744                attributes: Vec::new(),
13745            }),
13746        ];
13747        let set = protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
13748            .expect("test operation should succeed");
13749        assert_eq!(
13750            set,
13751            RefAdvertisementSet {
13752                protocol: ProtocolVersion::V2,
13753                refs: vec![RefAdvertisement {
13754                    oid: main,
13755                    name: "refs/heads/main".into(),
13756                    capabilities: vec![Capability {
13757                        name: "symref".into(),
13758                        value: Some("HEAD:refs/heads/main".into()),
13759                    }],
13760                }],
13761                shallow: Vec::new(),
13762            }
13763        );
13764    }
13765}