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    if let Some(branch) = refname.strip_prefix("refs/heads/") {
516        Ok(format!("branch '{branch}'"))
517    } else if let Some(tag) = refname.strip_prefix("refs/tags/") {
518        Ok(format!("tag '{tag}'"))
519    } else {
520        Ok(refname.to_string())
521    }
522}
523
524pub fn fetch_head_remote_description(refname: &str, remote: &str) -> Result<String> {
525    validate_fetch_head_description_field(remote)?;
526    Ok(format!(
527        "{} of {remote}",
528        fetch_head_ref_description(refname)?
529    ))
530}
531
532pub fn parse_fetch_head(format: ObjectFormat, input: &[u8]) -> Result<Vec<FetchHeadRecord>> {
533    if input.is_empty() {
534        return Ok(Vec::new());
535    }
536    input
537        .split_inclusive(|byte| *byte == b'\n')
538        .map(|line| parse_fetch_head_record(format, line))
539        .collect()
540}
541
542pub fn encode_fetch_head(records: &[FetchHeadRecord]) -> Result<Vec<u8>> {
543    let mut out = Vec::new();
544    for record in records {
545        validate_fetch_head_description_field(&record.description)?;
546        out.extend_from_slice(record.oid.to_string().as_bytes());
547        out.push(b'\t');
548        if record.not_for_merge {
549            out.extend_from_slice(b"not-for-merge");
550        }
551        out.push(b'\t');
552        out.extend_from_slice(record.description.as_bytes());
553        out.push(b'\n');
554    }
555    Ok(out)
556}
557
558pub fn read_fetch_head(
559    format: ObjectFormat,
560    reader: &mut impl Read,
561) -> Result<Vec<FetchHeadRecord>> {
562    let mut input = Vec::new();
563    reader.read_to_end(&mut input)?;
564    parse_fetch_head(format, &input)
565}
566
567pub fn write_fetch_head(writer: &mut impl Write, records: &[FetchHeadRecord]) -> Result<()> {
568    for record in records {
569        validate_fetch_head_description_field(&record.description)?;
570        writer.write_all(record.oid.to_string().as_bytes())?;
571        writer.write_all(b"\t")?;
572        if record.not_for_merge {
573            writer.write_all(b"not-for-merge")?;
574        }
575        writer.write_all(b"\t")?;
576        writer.write_all(record.description.as_bytes())?;
577        writer.write_all(b"\n")?;
578    }
579    Ok(())
580}
581
582/// Match an abbreviated refspec source against the advertised refs the way
583/// upstream's `find_ref_by_name_abbrev` (remote.c) does: score each
584/// advertisement with `refname_match`'s `ref_rev_parse_rules` (exact name
585/// first, then `refs/<name>`, `refs/tags/<name>`, `refs/heads/<name>`,
586/// `refs/remotes/<name>`, `refs/remotes/<name>/HEAD`) and keep the best.
587fn find_advertised_ref_by_name_abbrev<'a>(
588    refs: &'a [RefAdvertisement],
589    name: &str,
590) -> Option<&'a RefAdvertisement> {
591    let mut best: Option<(&RefAdvertisement, usize)> = None;
592    for reference in refs {
593        let score = fetch_refname_match_score(name, &reference.name);
594        if score > best.map(|(_, score)| score).unwrap_or(0) {
595            best = Some((reference, score));
596        }
597    }
598    best.map(|(reference, _)| reference)
599}
600
601/// `refname_match` (refs.c): non-zero when `abbrev` can mean `full`, with the
602/// magnitude giving disambiguation precedence (earlier rules win).
603fn fetch_refname_match_score(abbrev: &str, full: &str) -> usize {
604    let expansions = [
605        abbrev.to_string(),
606        format!("refs/{abbrev}"),
607        format!("refs/tags/{abbrev}"),
608        format!("refs/heads/{abbrev}"),
609        format!("refs/remotes/{abbrev}"),
610        format!("refs/remotes/{abbrev}/HEAD"),
611    ];
612    for (index, candidate) in expansions.iter().enumerate() {
613        if candidate == full {
614            return expansions.len() - index;
615        }
616    }
617    0
618}
619
620/// Qualify a fetch refspec destination the way upstream's `get_local_ref`
621/// (remote.c) does: `refs/...` stays as-is, `heads/`, `tags/` and `remotes/`
622/// gain a `refs/` prefix, and anything else lands under `refs/heads/`.
623fn fetch_local_ref_name(name: &str) -> String {
624    if name.starts_with("refs/") {
625        name.to_string()
626    } else if name.starts_with("heads/")
627        || name.starts_with("tags/")
628        || name.starts_with("remotes/")
629    {
630        format!("refs/{name}")
631    } else {
632        format!("refs/heads/{name}")
633    }
634}
635
636pub fn plan_fetch_ref_updates(
637    refs: &[RefAdvertisement],
638    refspecs: &[RefSpec],
639    auto_follow_tags: bool,
640) -> Result<Vec<FetchRefUpdate>> {
641    let negative = refspecs
642        .iter()
643        .filter(|refspec| refspec.negative)
644        .collect::<Vec<_>>();
645    let mut updates = Vec::new();
646    for refspec in refspecs.iter().filter(|refspec| !refspec.negative) {
647        validate_refspec_shape(refspec)?;
648        let Some(src) = refspec.src.as_deref() else {
649            return Err(GitError::InvalidFormat(
650                "fetch refspec is missing a source".into(),
651            ));
652        };
653        if refspec.pattern {
654            for reference in refs {
655                if refspec_is_excluded(&negative, &reference.name)? {
656                    continue;
657                }
658                if let Some(dst) = refspec_map_source(refspec, &reference.name)? {
659                    updates.push(FetchRefUpdate {
660                        src: reference.name.clone(),
661                        dst: Some(dst),
662                        oid: reference.oid,
663                        not_for_merge: false,
664                    });
665                }
666            }
667            continue;
668        }
669        if refspec_is_excluded(&negative, src)? {
670            continue;
671        }
672        let Some(reference) = find_advertised_ref_by_name_abbrev(refs, src) else {
673            return Err(GitError::reference_not_found(format!("remote ref {src}")));
674        };
675        updates.push(FetchRefUpdate {
676            src: reference.name.clone(),
677            dst: refspec.dst.as_deref().map(fetch_local_ref_name),
678            oid: reference.oid,
679            not_for_merge: false,
680        });
681    }
682    if auto_follow_tags && updates.iter().any(|update| update.dst.is_some()) {
683        let fetched_oids = updates.iter().map(|update| update.oid).collect::<Vec<_>>();
684        let fetched_srcs = updates
685            .iter()
686            .map(|update| update.src.clone())
687            .collect::<Vec<_>>();
688        for reference in refs {
689            if reference.name.starts_with("refs/tags/")
690                && fetched_oids.iter().any(|oid| oid == &reference.oid)
691                && !fetched_srcs.contains(&reference.name)
692                && !refspec_is_excluded(&negative, &reference.name)?
693            {
694                updates.push(FetchRefUpdate {
695                    src: reference.name.clone(),
696                    dst: Some(reference.name.clone()),
697                    oid: reference.oid,
698                    not_for_merge: true,
699                });
700            }
701        }
702    }
703    Ok(updates)
704}
705
706pub fn fetch_ref_updates_to_fetch_head(
707    updates: &[FetchRefUpdate],
708    remote: &str,
709) -> Result<Vec<FetchHeadRecord>> {
710    updates
711        .iter()
712        .map(|update| {
713            Ok(FetchHeadRecord {
714                oid: update.oid,
715                not_for_merge: update.not_for_merge,
716                description: fetch_head_remote_description(&update.src, remote)?,
717            })
718        })
719        .collect()
720}
721
722pub fn plan_push_commands(
723    format: ObjectFormat,
724    local_refs: &[PushSourceRef],
725    remote_refs: &[RefAdvertisement],
726    refspecs: &[RefSpec],
727) -> Result<Vec<ReceivePackCommand>> {
728    let zero = zero_object_id(format)?;
729    let mut commands = Vec::new();
730    for refspec in refspecs {
731        validate_refspec_shape(refspec)?;
732        if refspec.negative {
733            return Err(GitError::InvalidFormat(
734                "push refspec must not be negative".into(),
735            ));
736        }
737        match (refspec.src.as_deref(), refspec.dst.as_deref()) {
738            (None, None) => {
739                for local in local_refs {
740                    validate_push_source_ref(format, local)?;
741                    if let Some(remote) = remote_ref(remote_refs, &local.name) {
742                        commands.push(ReceivePackCommand {
743                            old_id: remote.oid,
744                            new_id: local.oid,
745                            name: local.name.clone(),
746                        });
747                    }
748                }
749            }
750            (None, Some(dst)) => {
751                validate_refspec_endpoint("push destination", dst)?;
752                let remote = remote_ref(remote_refs, dst)
753                    .ok_or_else(|| GitError::reference_not_found(format!("remote ref {dst}")))?;
754                commands.push(ReceivePackCommand {
755                    old_id: remote.oid,
756                    new_id: zero.clone(),
757                    name: dst.to_string(),
758                });
759            }
760            (Some(src), dst) if refspec.pattern => {
761                let Some((src_prefix, src_suffix)) = src.split_once('*') else {
762                    return Err(GitError::InvalidFormat(
763                        "pattern push refspec source is missing wildcard".into(),
764                    ));
765                };
766                let dst = dst.ok_or_else(|| {
767                    GitError::InvalidFormat("pattern push refspec is missing destination".into())
768                })?;
769                let (dst_prefix, dst_suffix) = dst.split_once('*').ok_or_else(|| {
770                    GitError::InvalidFormat(
771                        "pattern push refspec destination is missing wildcard".into(),
772                    )
773                })?;
774                for local in local_refs {
775                    validate_push_source_ref(format, local)?;
776                    let Some(middle) = local
777                        .name
778                        .strip_prefix(src_prefix)
779                        .and_then(|value| value.strip_suffix(src_suffix))
780                    else {
781                        continue;
782                    };
783                    let name = format!("{dst_prefix}{middle}{dst_suffix}");
784                    let old_id = remote_ref(remote_refs, &name)
785                        .map(|reference| reference.oid)
786                        .unwrap_or_else(|| zero.clone());
787                    commands.push(ReceivePackCommand {
788                        old_id,
789                        new_id: local.oid,
790                        name,
791                    });
792                }
793            }
794            (Some(src), dst) => {
795                validate_refspec_endpoint("push source", src)?;
796                let local = local_ref(local_refs, src)
797                    .ok_or_else(|| GitError::reference_not_found(format!("local ref {src}")))?;
798                validate_push_source_ref(format, local)?;
799                let name = dst.unwrap_or(src);
800                validate_refspec_endpoint("push destination", name)?;
801                let old_id = remote_ref(remote_refs, name)
802                    .map(|reference| reference.oid)
803                    .unwrap_or_else(|| zero.clone());
804                commands.push(ReceivePackCommand {
805                    old_id,
806                    new_id: local.oid,
807                    name: name.to_string(),
808                });
809            }
810        }
811    }
812    Ok(commands)
813}
814
815pub fn build_receive_pack_push_request(
816    features: &ReceivePackFeatures,
817    commands: Vec<ReceivePackCommand>,
818    packfile: Vec<u8>,
819    options: ReceivePackPushRequestOptions,
820) -> Result<ReceivePackPushRequest> {
821    let mut capabilities = Vec::new();
822    if options.report_status_v2 {
823        require_receive_pack_feature(features.report_status_v2, "report-status-v2")?;
824        capabilities.push(Capability {
825            name: "report-status-v2".into(),
826            value: None,
827        });
828    } else if options.report_status {
829        require_receive_pack_feature(features.report_status, "report-status")?;
830        capabilities.push(Capability {
831            name: "report-status".into(),
832            value: None,
833        });
834    }
835    if commands.iter().any(is_receive_pack_delete_command) {
836        require_receive_pack_feature(features.delete_refs, "delete-refs")?;
837        capabilities.push(Capability {
838            name: "delete-refs".into(),
839            value: None,
840        });
841    }
842    if options.atomic {
843        require_receive_pack_feature(features.atomic, "atomic")?;
844        capabilities.push(Capability {
845            name: "atomic".into(),
846            value: None,
847        });
848    }
849    if options.ofs_delta {
850        require_receive_pack_feature(features.ofs_delta, "ofs-delta")?;
851        capabilities.push(Capability {
852            name: "ofs-delta".into(),
853            value: None,
854        });
855    }
856    if options.side_band_64k {
857        require_receive_pack_feature(features.side_band_64k, "side-band-64k")?;
858        capabilities.push(Capability {
859            name: "side-band-64k".into(),
860            value: None,
861        });
862    }
863    if options.quiet {
864        require_receive_pack_feature(features.quiet, "quiet")?;
865        capabilities.push(Capability {
866            name: "quiet".into(),
867            value: None,
868        });
869    }
870    if let Some(agent) = &options.agent {
871        validate_capability_field("receive-pack request agent", agent)?;
872        capabilities.push(Capability {
873            name: "agent".into(),
874            value: Some(agent.clone()),
875        });
876    }
877    if let Some(format) = options.object_format {
878        if features.object_format != Some(format) {
879            return Err(GitError::InvalidFormat(
880                "receive-pack request object-format was not advertised".into(),
881            ));
882        }
883        capabilities.push(Capability {
884            name: "object-format".into(),
885            value: Some(format.name().into()),
886        });
887    }
888    let push_options = if options.push_options.is_empty() {
889        None
890    } else {
891        require_receive_pack_feature(features.push_options, "push-options")?;
892        for option in &options.push_options {
893            validate_receive_pack_push_option(option.as_bytes())?;
894        }
895        capabilities.push(Capability {
896            name: "push-options".into(),
897            value: None,
898        });
899        Some(options.push_options)
900    };
901    let request = ReceivePackPushRequest {
902        commands: ReceivePackRequest {
903            commands,
904            capabilities,
905            shallow: Vec::new(),
906        },
907        push_options,
908        packfile,
909    };
910    validate_receive_pack_push_request_features(features, &request)?;
911    Ok(request)
912}
913
914pub fn smart_http_info_refs_path(repository_path: &str, service: GitService) -> Result<String> {
915    validate_smart_http_service(service)?;
916    let repository_path = normalize_http_repository_path(repository_path)?;
917    Ok(format!(
918        "{repository_path}/info/refs?service={}",
919        service.as_str()
920    ))
921}
922
923pub fn smart_http_rpc_path(repository_path: &str, service: GitService) -> Result<String> {
924    validate_smart_http_service(service)?;
925    let repository_path = normalize_http_repository_path(repository_path)?;
926    Ok(format!("{repository_path}/{}", service.as_str()))
927}
928
929pub fn dumb_http_info_refs_path(repository_path: &str) -> Result<String> {
930    let repository_path = normalize_http_repository_path(repository_path)?;
931    Ok(format!("{repository_path}/info/refs"))
932}
933
934pub fn dumb_http_alternates_path(repository_path: &str) -> Result<String> {
935    let repository_path = normalize_http_repository_path(repository_path)?;
936    Ok(format!("{repository_path}/objects/info/http-alternates"))
937}
938
939pub fn dumb_http_packs_path(repository_path: &str) -> Result<String> {
940    let repository_path = normalize_http_repository_path(repository_path)?;
941    Ok(format!("{repository_path}/objects/info/packs"))
942}
943
944pub fn dumb_http_loose_object_path(repository_path: &str, oid: &ObjectId) -> Result<String> {
945    let repository_path = normalize_http_repository_path(repository_path)?;
946    let oid = oid.to_string();
947    let (directory, file) = oid.split_at(2);
948    Ok(format!("{repository_path}/objects/{directory}/{file}"))
949}
950
951pub fn dumb_http_pack_file_path(repository_path: &str, hash: &ObjectId) -> Result<String> {
952    dumb_http_pack_resource_path(repository_path, hash, "pack")
953}
954
955pub fn dumb_http_pack_index_path(repository_path: &str, hash: &ObjectId) -> Result<String> {
956    dumb_http_pack_resource_path(repository_path, hash, "idx")
957}
958
959pub fn smart_http_advertisement_content_type(service: GitService) -> Result<String> {
960    validate_smart_http_service(service)?;
961    Ok(format!("application/x-{}-advertisement", service.as_str()))
962}
963
964pub fn smart_http_rpc_request_content_type(service: GitService) -> Result<String> {
965    validate_smart_http_service(service)?;
966    Ok(format!("application/x-{}-request", service.as_str()))
967}
968
969pub fn smart_http_rpc_result_content_type(service: GitService) -> Result<String> {
970    validate_smart_http_service(service)?;
971    Ok(format!("application/x-{}-result", service.as_str()))
972}
973
974pub fn parse_smart_http_advertisement_content_type(value: &str) -> Result<GitService> {
975    parse_smart_http_content_type(value, "-advertisement")
976}
977
978pub fn parse_smart_http_rpc_request_content_type(value: &str) -> Result<GitService> {
979    parse_smart_http_content_type(value, "-request")
980}
981
982pub fn parse_smart_http_rpc_result_content_type(value: &str) -> Result<GitService> {
983    parse_smart_http_content_type(value, "-result")
984}
985
986pub fn parse_sideband_packet(payload: &[u8]) -> Result<SideBandPacket> {
987    let Some((&channel, data)) = payload.split_first() else {
988        return Err(GitError::InvalidFormat("sideband packet is empty".into()));
989    };
990    let channel = match channel {
991        1 => SideBandChannel::Data,
992        2 => SideBandChannel::Progress,
993        3 => SideBandChannel::Fatal,
994        other => {
995            return Err(GitError::InvalidFormat(format!(
996                "invalid sideband channel {other}"
997            )));
998        }
999    };
1000    Ok(SideBandPacket {
1001        channel,
1002        data: data.to_vec(),
1003    })
1004}
1005
1006pub fn encode_sideband_packet(packet: &SideBandPacket) -> Result<Vec<u8>> {
1007    let mut out = Vec::with_capacity(packet.data.len() + 1);
1008    out.push(match packet.channel {
1009        SideBandChannel::Data => 1,
1010        SideBandChannel::Progress => 2,
1011        SideBandChannel::Fatal => 3,
1012    });
1013    out.extend_from_slice(&packet.data);
1014    if out.len() > PKT_LINE_MAX_PAYLOAD_LEN {
1015        return Err(GitError::InvalidFormat(format!(
1016            "sideband packet exceeds {PKT_LINE_MAX_PAYLOAD_LEN} bytes"
1017        )));
1018    }
1019    Ok(out)
1020}
1021
1022pub fn write_sideband_packet(writer: &mut impl Write, packet: &SideBandPacket) -> Result<()> {
1023    write_sideband_payload(writer, packet.channel, &packet.data)
1024}
1025
1026fn write_sideband_payload(
1027    writer: &mut impl Write,
1028    channel: SideBandChannel,
1029    data: &[u8],
1030) -> Result<()> {
1031    let payload_len = data
1032        .len()
1033        .checked_add(1)
1034        .ok_or_else(|| GitError::InvalidFormat("sideband packet length overflow".into()))?;
1035    if payload_len > PKT_LINE_MAX_PAYLOAD_LEN {
1036        return Err(GitError::InvalidFormat(format!(
1037            "sideband packet exceeds {PKT_LINE_MAX_PAYLOAD_LEN} bytes"
1038        )));
1039    }
1040    writer.write_all(&pkt_line_header(payload_len + 4))?;
1041    writer.write_all(&[match channel {
1042        SideBandChannel::Data => 1,
1043        SideBandChannel::Progress => 2,
1044        SideBandChannel::Fatal => 3,
1045    }])?;
1046    writer.write_all(data)?;
1047    Ok(())
1048}
1049
1050pub fn parse_sideband_packets(payloads: &[Vec<u8>]) -> Result<Vec<SideBandPacket>> {
1051    payloads
1052        .iter()
1053        .map(|payload| parse_sideband_packet(payload))
1054        .collect()
1055}
1056
1057pub fn encode_sideband_packets(packets: &[SideBandPacket]) -> Result<Vec<Vec<u8>>> {
1058    packets.iter().map(encode_sideband_packet).collect()
1059}
1060
1061pub fn parse_sideband_stream(frames: &[PktLineFrame]) -> Result<Vec<SideBandPacket>> {
1062    let mut packets = Vec::new();
1063    let mut saw_flush = false;
1064    for (idx, frame) in frames.iter().enumerate() {
1065        match frame {
1066            PktLineFrame::Data(payload) if !saw_flush => {
1067                packets.push(parse_sideband_packet(payload)?);
1068            }
1069            PktLineFrame::Data(_) => {
1070                return Err(GitError::InvalidFormat(
1071                    "sideband stream has data after flush".into(),
1072                ));
1073            }
1074            PktLineFrame::Flush => {
1075                saw_flush = true;
1076                if idx + 1 != frames.len() {
1077                    return Err(GitError::InvalidFormat(
1078                        "sideband stream has frames after flush".into(),
1079                    ));
1080                }
1081            }
1082            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
1083                return Err(GitError::InvalidFormat(
1084                    "sideband stream contains a non-flush control packet".into(),
1085                ));
1086            }
1087        }
1088    }
1089    if !saw_flush {
1090        return Err(GitError::InvalidFormat(
1091            "sideband stream missing flush".into(),
1092        ));
1093    }
1094    Ok(packets)
1095}
1096
1097pub fn encode_sideband_stream(packets: &[SideBandPacket]) -> Result<Vec<PktLineFrame>> {
1098    let mut frames = Vec::new();
1099    for packet in packets {
1100        frames.push(PktLineFrame::data(encode_sideband_packet(packet)?)?);
1101    }
1102    frames.push(PktLineFrame::Flush);
1103    Ok(frames)
1104}
1105
1106pub fn read_sideband_stream(reader: &mut impl Read) -> Result<Vec<SideBandPacket>> {
1107    let frames = read_pkt_line_frames_until_flush(reader)?;
1108    parse_sideband_stream(&frames)
1109}
1110
1111pub fn write_sideband_stream(writer: &mut impl Write, packets: &[SideBandPacket]) -> Result<()> {
1112    for packet in packets {
1113        write_sideband_packet(writer, packet)?;
1114    }
1115    writer.write_all(b"0000")?;
1116    Ok(())
1117}
1118
1119pub fn demux_sideband_packets(packets: &[SideBandPacket]) -> Result<SideBandDemux> {
1120    let mut out = SideBandDemux::default();
1121    for packet in packets {
1122        match packet.channel {
1123            SideBandChannel::Data => out.data.extend_from_slice(&packet.data),
1124            SideBandChannel::Progress => out.progress.push(packet.data.clone()),
1125            SideBandChannel::Fatal => {
1126                let message = String::from_utf8_lossy(&packet.data).into_owned();
1127                return Err(GitError::InvalidFormat(format!(
1128                    "sideband fatal: {message}"
1129                )));
1130            }
1131        }
1132    }
1133    Ok(out)
1134}
1135
1136pub fn parse_and_demux_sideband_packets(payloads: &[Vec<u8>]) -> Result<SideBandDemux> {
1137    let packets = parse_sideband_packets(payloads)?;
1138    demux_sideband_packets(&packets)
1139}
1140
1141pub fn demux_sideband_stream(frames: &[PktLineFrame]) -> Result<SideBandDemux> {
1142    let packets = parse_sideband_stream(frames)?;
1143    demux_sideband_packets(&packets)
1144}
1145
1146pub fn read_and_demux_sideband_stream(reader: &mut impl Read) -> Result<SideBandDemux> {
1147    let packets = read_sideband_stream(reader)?;
1148    demux_sideband_packets(&packets)
1149}
1150
1151pub fn parse_upload_archive_request(frames: &[PktLineFrame]) -> Result<UploadArchiveRequest> {
1152    let mut request = UploadArchiveRequest::default();
1153    let mut saw_flush = false;
1154    for (idx, frame) in frames.iter().enumerate() {
1155        match frame {
1156            PktLineFrame::Data(payload) if !saw_flush => {
1157                let text = parse_protocol_v2_line_text("upload-archive request argument", payload)?;
1158                let argument = text.strip_prefix("argument ").ok_or_else(|| {
1159                    GitError::InvalidFormat("upload-archive request line must be argument".into())
1160                })?;
1161                validate_upload_archive_argument(argument)?;
1162                request.arguments.push(argument.to_string());
1163            }
1164            PktLineFrame::Data(_) => {
1165                return Err(GitError::InvalidFormat(
1166                    "upload-archive request has data after flush".into(),
1167                ));
1168            }
1169            PktLineFrame::Flush => {
1170                saw_flush = true;
1171                if idx + 1 != frames.len() {
1172                    return Err(GitError::InvalidFormat(
1173                        "upload-archive request has frames after flush".into(),
1174                    ));
1175                }
1176            }
1177            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
1178                return Err(GitError::InvalidFormat(
1179                    "upload-archive request contains a non-flush control packet".into(),
1180                ));
1181            }
1182        }
1183    }
1184    if !saw_flush {
1185        return Err(GitError::InvalidFormat(
1186            "upload-archive request missing flush".into(),
1187        ));
1188    }
1189    if request.arguments.is_empty() {
1190        return Err(GitError::InvalidFormat(
1191            "upload-archive request is missing arguments".into(),
1192        ));
1193    }
1194    Ok(request)
1195}
1196
1197pub fn encode_upload_archive_request(request: &UploadArchiveRequest) -> Result<Vec<PktLineFrame>> {
1198    if request.arguments.is_empty() {
1199        return Err(GitError::InvalidFormat(
1200            "upload-archive request is missing arguments".into(),
1201        ));
1202    }
1203    let mut frames = Vec::new();
1204    for argument in &request.arguments {
1205        validate_upload_archive_argument(argument)?;
1206        frames.push(PktLineFrame::data(line_from_str(&format!(
1207            "argument {argument}"
1208        )))?);
1209    }
1210    frames.push(PktLineFrame::Flush);
1211    Ok(frames)
1212}
1213
1214pub fn read_upload_archive_request(reader: &mut impl Read) -> Result<UploadArchiveRequest> {
1215    let frames = read_pkt_line_frames_until_flush(reader)?;
1216    parse_upload_archive_request(&frames)
1217}
1218
1219pub fn write_upload_archive_request(
1220    writer: &mut impl Write,
1221    request: &UploadArchiveRequest,
1222) -> Result<()> {
1223    if request.arguments.is_empty() {
1224        return Err(GitError::InvalidFormat(
1225            "upload-archive request is missing arguments".into(),
1226        ));
1227    }
1228    for argument in &request.arguments {
1229        validate_upload_archive_argument(argument)?;
1230        write_pkt_line_payload(writer, &line_from_str(&format!("argument {argument}")))?;
1231    }
1232    writer.write_all(b"0000")?;
1233    Ok(())
1234}
1235
1236pub fn parse_upload_archive_response(frames: &[PktLineFrame]) -> Result<UploadArchiveResponse> {
1237    let Some((first, rest)) = frames.split_first() else {
1238        return Err(GitError::InvalidFormat(
1239            "upload-archive response is empty".into(),
1240        ));
1241    };
1242    let PktLineFrame::Data(payload) = first else {
1243        return Err(GitError::InvalidFormat(
1244            "upload-archive response must start with a data packet".into(),
1245        ));
1246    };
1247    let text = parse_protocol_v2_line_text("upload-archive response status", payload)?;
1248    if text == "ACK" {
1249        return Ok(UploadArchiveResponse::Ack {
1250            sideband: parse_sideband_stream(rest)?,
1251        });
1252    }
1253    if let Some(message) = text.strip_prefix("NACK ") {
1254        validate_upload_archive_status_message(message)?;
1255        if !matches!(rest, [PktLineFrame::Flush]) {
1256            return Err(GitError::InvalidFormat(
1257                "upload-archive NACK response must end with flush".into(),
1258            ));
1259        }
1260        return Ok(UploadArchiveResponse::Nack {
1261            message: message.to_string(),
1262        });
1263    }
1264    Err(GitError::InvalidFormat(format!(
1265        "unsupported upload-archive response status {text}"
1266    )))
1267}
1268
1269pub fn encode_upload_archive_response(
1270    response: &UploadArchiveResponse,
1271) -> Result<Vec<PktLineFrame>> {
1272    let mut frames = Vec::new();
1273    match response {
1274        UploadArchiveResponse::Ack { sideband } => {
1275            frames.push(PktLineFrame::data(line_from_str("ACK"))?);
1276            frames.extend(encode_sideband_stream(sideband)?);
1277        }
1278        UploadArchiveResponse::Nack { message } => {
1279            validate_upload_archive_status_message(message)?;
1280            frames.push(PktLineFrame::data(line_from_str(&format!(
1281                "NACK {message}"
1282            )))?);
1283            frames.push(PktLineFrame::Flush);
1284        }
1285    }
1286    Ok(frames)
1287}
1288
1289pub fn read_upload_archive_response(reader: &mut impl Read) -> Result<UploadArchiveResponse> {
1290    let frames = read_pkt_line_frames_until_flush(reader)?;
1291    parse_upload_archive_response(&frames)
1292}
1293
1294pub fn write_upload_archive_response(
1295    writer: &mut impl Write,
1296    response: &UploadArchiveResponse,
1297) -> Result<()> {
1298    match response {
1299        UploadArchiveResponse::Ack { sideband } => {
1300            write_pkt_line_payload(writer, b"ACK\n")?;
1301            write_sideband_stream(writer, sideband)?;
1302        }
1303        UploadArchiveResponse::Nack { message } => {
1304            validate_upload_archive_status_message(message)?;
1305            write_pkt_line_payload(writer, &line_from_str(&format!("NACK {message}")))?;
1306            writer.write_all(b"0000")?;
1307        }
1308    }
1309    Ok(())
1310}
1311
1312pub fn demux_upload_archive_response(response: &UploadArchiveResponse) -> Result<SideBandDemux> {
1313    match response {
1314        UploadArchiveResponse::Ack { sideband } => demux_sideband_packets(sideband),
1315        UploadArchiveResponse::Nack { message } => Err(GitError::InvalidFormat(format!(
1316            "upload-archive NACK: {message}"
1317        ))),
1318    }
1319}
1320
1321fn parse_pkt_len(bytes: &[u8]) -> Result<usize> {
1322    let mut len = 0usize;
1323    for byte in bytes {
1324        len = (len << 4) | hex_nibble(*byte)? as usize;
1325    }
1326    Ok(len)
1327}
1328
1329fn hex_nibble(byte: u8) -> Result<u8> {
1330    match byte {
1331        b'0'..=b'9' => Ok(byte - b'0'),
1332        b'a'..=b'f' => Ok(byte - b'a' + 10),
1333        b'A'..=b'F' => Ok(byte - b'A' + 10),
1334        _ => Err(GitError::InvalidFormat(format!(
1335            "invalid pkt-line length byte {byte:#04x}"
1336        ))),
1337    }
1338}
1339
1340#[derive(Debug, Clone, PartialEq, Eq)]
1341pub struct TransportHandshake {
1342    pub protocol: ProtocolVersion,
1343    pub capabilities: Vec<Capability>,
1344}
1345
1346#[derive(Debug, Clone, PartialEq, Eq)]
1347pub struct RefAdvertisement {
1348    pub oid: ObjectId,
1349    pub name: String,
1350    pub capabilities: Vec<Capability>,
1351}
1352
1353#[derive(Debug, Clone, PartialEq, Eq)]
1354pub struct DumbHttpRefRecord {
1355    pub oid: ObjectId,
1356    pub name: String,
1357    pub peeled: bool,
1358}
1359
1360#[derive(Debug, Clone, PartialEq, Eq)]
1361pub struct DumbHttpPackRecord {
1362    pub hash: ObjectId,
1363}
1364
1365#[derive(Debug, Clone, PartialEq, Eq)]
1366pub struct RefAdvertisementSet {
1367    pub protocol: ProtocolVersion,
1368    pub refs: Vec<RefAdvertisement>,
1369    pub shallow: Vec<ObjectId>,
1370}
1371
1372#[derive(Debug, Clone, PartialEq, Eq, Default)]
1373pub struct UploadPackRequest {
1374    pub wants: Vec<ObjectId>,
1375    pub capabilities: Vec<Capability>,
1376    pub shallow: Vec<ObjectId>,
1377    pub deepen: Option<u32>,
1378    pub deepen_since: Option<u64>,
1379    pub deepen_not: Vec<String>,
1380    pub filter: Option<String>,
1381}
1382
1383#[derive(Debug, Clone, PartialEq, Eq, Default)]
1384pub struct UploadPackFeatures {
1385    pub multi_ack: bool,
1386    pub multi_ack_detailed: bool,
1387    pub no_done: bool,
1388    pub thin_pack: bool,
1389    pub side_band: bool,
1390    pub side_band_64k: bool,
1391    pub ofs_delta: bool,
1392    pub shallow: bool,
1393    pub deepen_since: bool,
1394    pub deepen_not: bool,
1395    pub include_tag: bool,
1396    pub no_progress: bool,
1397    pub allow_tip_sha1_in_want: bool,
1398    pub allow_reachable_sha1_in_want: bool,
1399    pub filter: bool,
1400    pub agent: Option<String>,
1401    pub object_format: Option<ObjectFormat>,
1402    pub symrefs: Vec<String>,
1403    pub unknown: Vec<Capability>,
1404}
1405
1406#[derive(Debug, Clone, PartialEq, Eq, Default)]
1407pub struct UploadPackNegotiationRequest {
1408    pub haves: Vec<ObjectId>,
1409    pub done: bool,
1410}
1411
1412#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1413pub enum UploadPackAckStatus {
1414    Continue,
1415    Common,
1416    Ready,
1417}
1418
1419#[derive(Debug, Clone, PartialEq, Eq)]
1420pub enum UploadPackAcknowledgment {
1421    Nak,
1422    Ack {
1423        oid: ObjectId,
1424        status: Option<UploadPackAckStatus>,
1425    },
1426}
1427
1428#[derive(Debug, Clone, PartialEq, Eq, Default)]
1429pub struct UploadPackPackfileResponse {
1430    pub acknowledgments: Vec<UploadPackAcknowledgment>,
1431    pub sideband: Vec<SideBandPacket>,
1432}
1433
1434#[derive(Debug, Clone, PartialEq, Eq, Default)]
1435pub struct UploadPackRawPackfileResponse {
1436    pub acknowledgments: Vec<UploadPackAcknowledgment>,
1437    pub packfile: Vec<u8>,
1438}
1439
1440#[derive(Debug, Clone, PartialEq, Eq)]
1441pub struct ReceivePackCommand {
1442    pub old_id: ObjectId,
1443    pub new_id: ObjectId,
1444    pub name: String,
1445}
1446
1447#[derive(Debug, Clone, PartialEq, Eq, Default)]
1448pub struct ReceivePackRequest {
1449    pub shallow: Vec<ObjectId>,
1450    pub commands: Vec<ReceivePackCommand>,
1451    pub capabilities: Vec<Capability>,
1452}
1453
1454#[derive(Debug, Clone, PartialEq, Eq, Default)]
1455pub struct ReceivePackPushRequest {
1456    pub commands: ReceivePackRequest,
1457    pub push_options: Option<Vec<String>>,
1458    pub packfile: Vec<u8>,
1459}
1460
1461#[derive(Debug, Clone, PartialEq, Eq, Default)]
1462pub struct ReceivePackPushRequestOptions {
1463    pub report_status: bool,
1464    pub report_status_v2: bool,
1465    pub atomic: bool,
1466    pub ofs_delta: bool,
1467    pub side_band_64k: bool,
1468    pub quiet: bool,
1469    pub agent: Option<String>,
1470    pub object_format: Option<ObjectFormat>,
1471    pub push_options: Vec<String>,
1472}
1473
1474#[derive(Debug, Clone, PartialEq, Eq, Default)]
1475pub struct ReceivePackFeatures {
1476    pub report_status: bool,
1477    pub report_status_v2: bool,
1478    pub delete_refs: bool,
1479    pub ofs_delta: bool,
1480    pub atomic: bool,
1481    pub push_options: bool,
1482    pub side_band_64k: bool,
1483    pub quiet: bool,
1484    pub no_thin: bool,
1485    pub agent: Option<String>,
1486    pub object_format: Option<ObjectFormat>,
1487    pub unknown: Vec<Capability>,
1488}
1489
1490#[derive(Debug, Clone, PartialEq, Eq)]
1491pub enum ReceivePackUnpackStatus {
1492    Ok,
1493    Error(String),
1494}
1495
1496#[derive(Debug, Clone, PartialEq, Eq)]
1497pub enum ReceivePackCommandStatus {
1498    Ok { name: String },
1499    Ng { name: String, message: String },
1500}
1501
1502#[derive(Debug, Clone, PartialEq, Eq)]
1503pub struct ReceivePackReportStatus {
1504    pub unpack: ReceivePackUnpackStatus,
1505    pub commands: Vec<ReceivePackCommandStatus>,
1506}
1507
1508#[derive(Debug, Clone, PartialEq, Eq, Default)]
1509pub struct ReceivePackCommandStatusV2Options {
1510    pub refname: Option<String>,
1511    pub old_oid: Option<ObjectId>,
1512    pub new_oid: Option<ObjectId>,
1513    pub forced_update: bool,
1514}
1515
1516#[derive(Debug, Clone, PartialEq, Eq)]
1517pub enum ReceivePackCommandStatusV2 {
1518    Ok {
1519        name: String,
1520        options: ReceivePackCommandStatusV2Options,
1521    },
1522    Ng {
1523        name: String,
1524        message: String,
1525    },
1526}
1527
1528#[derive(Debug, Clone, PartialEq, Eq)]
1529pub struct ReceivePackReportStatusV2 {
1530    pub unpack: ReceivePackUnpackStatus,
1531    pub commands: Vec<ReceivePackCommandStatusV2>,
1532}
1533
1534#[derive(Debug, Clone, PartialEq, Eq)]
1535pub struct ProtocolV2CommandRequest {
1536    pub command: String,
1537    pub capabilities: Vec<Capability>,
1538    pub arguments: Vec<Vec<u8>>,
1539}
1540
1541#[derive(Debug, Clone, PartialEq, Eq)]
1542pub enum ProtocolV2Request {
1543    Command(ProtocolV2CommandRequest),
1544    Done,
1545}
1546
1547#[derive(Debug, Clone, PartialEq, Eq)]
1548pub enum ProtocolV2Command {
1549    LsRefs(ProtocolV2LsRefsRequest),
1550    Fetch(ProtocolV2FetchRequest),
1551    ObjectInfo(ProtocolV2ObjectInfoRequest),
1552    Unknown(ProtocolV2CommandRequest),
1553}
1554
1555#[derive(Debug, Clone, PartialEq, Eq)]
1556pub enum ProtocolV2SessionRequest {
1557    Command(ProtocolV2Command),
1558    Done,
1559}
1560
1561#[derive(Debug, Clone, PartialEq, Eq, Default)]
1562pub struct ProtocolV2CommandOptions {
1563    pub agent: Option<String>,
1564    pub object_format: Option<ObjectFormat>,
1565    pub server_options: Vec<String>,
1566    pub extra: Vec<Capability>,
1567}
1568
1569#[derive(Debug, Clone, PartialEq, Eq, Default)]
1570pub struct ProtocolV2FetchFeatures {
1571    pub shallow: bool,
1572    pub wait_for_done: bool,
1573    pub filter: bool,
1574    pub ref_in_want: bool,
1575    pub sideband_all: bool,
1576    pub packfile_uris: bool,
1577    pub unknown: Vec<String>,
1578}
1579
1580#[derive(Debug, Clone, PartialEq, Eq, Default)]
1581pub struct ProtocolV2LsRefsFeatures {
1582    pub unborn: bool,
1583    pub unknown: Vec<String>,
1584}
1585
1586impl ProtocolV2CommandRequest {
1587    pub fn new(command: impl Into<String>) -> Result<Self> {
1588        let command = command.into();
1589        validate_capability_name(&command)?;
1590        Ok(Self {
1591            command,
1592            capabilities: Vec::new(),
1593            arguments: Vec::new(),
1594        })
1595    }
1596}
1597
1598#[derive(Debug, Clone, PartialEq, Eq, Default)]
1599pub struct ProtocolV2LsRefsRequest {
1600    pub peel: bool,
1601    pub symrefs: bool,
1602    pub unborn: bool,
1603    pub ref_prefixes: Vec<String>,
1604}
1605
1606#[derive(Debug, Clone, PartialEq, Eq)]
1607pub struct ProtocolV2LsRefsRef {
1608    pub oid: ObjectId,
1609    pub name: String,
1610    pub peeled: Option<ObjectId>,
1611    pub symref_target: Option<String>,
1612    pub attributes: Vec<String>,
1613}
1614
1615#[derive(Debug, Clone, PartialEq, Eq)]
1616pub enum ProtocolV2LsRefsRecord {
1617    Ref(ProtocolV2LsRefsRef),
1618    Unborn {
1619        name: String,
1620        symref_target: Option<String>,
1621        attributes: Vec<String>,
1622    },
1623}
1624
1625#[derive(Debug, Clone, PartialEq, Eq, Default)]
1626pub struct ProtocolV2FetchRequest {
1627    pub wants: Vec<ObjectId>,
1628    pub want_refs: Vec<String>,
1629    pub haves: Vec<ObjectId>,
1630    pub shallow: Vec<ObjectId>,
1631    pub deepen: Option<u32>,
1632    pub deepen_since: Option<u64>,
1633    pub deepen_not: Vec<String>,
1634    pub deepen_relative: bool,
1635    pub filter: Option<String>,
1636    pub packfile_uris: Option<String>,
1637    pub thin_pack: bool,
1638    pub no_progress: bool,
1639    pub include_tag: bool,
1640    pub ofs_delta: bool,
1641    pub sideband_all: bool,
1642    pub wait_for_done: bool,
1643    pub done: bool,
1644}
1645
1646#[derive(Debug, Clone, PartialEq, Eq)]
1647pub enum ProtocolV2FetchAcknowledgment {
1648    Nak,
1649    Ack(ObjectId),
1650    Ready,
1651}
1652
1653#[derive(Debug, Clone, PartialEq, Eq)]
1654pub enum ProtocolV2FetchShallowInfo {
1655    Shallow(ObjectId),
1656    Unshallow(ObjectId),
1657}
1658
1659#[derive(Debug, Clone, PartialEq, Eq)]
1660pub struct ProtocolV2FetchWantedRef {
1661    pub oid: ObjectId,
1662    pub name: String,
1663}
1664
1665#[derive(Debug, Clone, PartialEq, Eq)]
1666pub struct ProtocolV2FetchPackfileUri {
1667    pub pack_hash: ObjectId,
1668    pub uri: String,
1669}
1670
1671#[derive(Debug, Clone, PartialEq, Eq)]
1672pub enum ProtocolV2FetchResponseSection {
1673    Acknowledgments(Vec<ProtocolV2FetchAcknowledgment>),
1674    ShallowInfo(Vec<ProtocolV2FetchShallowInfo>),
1675    WantedRefs(Vec<ProtocolV2FetchWantedRef>),
1676    PackfileUris(Vec<ProtocolV2FetchPackfileUri>),
1677    Packfile(Vec<Vec<u8>>),
1678    Unknown { name: String, lines: Vec<Vec<u8>> },
1679}
1680
1681#[derive(Debug, Clone, PartialEq, Eq, Default)]
1682pub struct ProtocolV2FetchSidebandAllResponse {
1683    pub sections: Vec<ProtocolV2FetchResponseSection>,
1684    pub progress: Vec<Vec<u8>>,
1685}
1686
1687#[derive(Debug, Clone, PartialEq, Eq, Default)]
1688pub struct ProtocolV2ObjectInfoRequest {
1689    pub size: bool,
1690    pub oids: Vec<ObjectId>,
1691}
1692
1693#[derive(Debug, Clone, PartialEq, Eq)]
1694pub struct ProtocolV2ObjectInfoRecord {
1695    pub oid: ObjectId,
1696    pub size: u64,
1697}
1698
1699#[derive(Debug, Clone, PartialEq, Eq, Default)]
1700pub struct ProtocolV2ObjectInfoResponse {
1701    pub size: bool,
1702    pub records: Vec<ProtocolV2ObjectInfoRecord>,
1703}
1704
1705impl ProtocolV2LsRefsRequest {
1706    pub fn from_command_request(request: &ProtocolV2CommandRequest) -> Result<Self> {
1707        if request.command != "ls-refs" {
1708            return Err(GitError::InvalidFormat(format!(
1709                "expected ls-refs command, got {}",
1710                request.command
1711            )));
1712        }
1713        let mut out = Self::default();
1714        for argument in &request.arguments {
1715            let text = std::str::from_utf8(argument)
1716                .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
1717            match text {
1718                "peel" => out.peel = true,
1719                "symrefs" => out.symrefs = true,
1720                "unborn" => out.unborn = true,
1721                value if value.starts_with("ref-prefix ") => {
1722                    let prefix = value
1723                        .strip_prefix("ref-prefix ")
1724                        .ok_or_else(|| GitError::InvalidFormat("invalid ref-prefix".into()))?;
1725                    validate_protocol_v2_token("ls-refs ref-prefix", prefix)?;
1726                    out.ref_prefixes.push(prefix.to_string());
1727                }
1728                other => {
1729                    return Err(GitError::InvalidFormat(format!(
1730                        "unsupported ls-refs argument {other}"
1731                    )));
1732                }
1733            }
1734        }
1735        Ok(out)
1736    }
1737
1738    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
1739        let mut request = ProtocolV2CommandRequest::new("ls-refs")?;
1740        if self.peel {
1741            request.arguments.push(b"peel".to_vec());
1742        }
1743        if self.symrefs {
1744            request.arguments.push(b"symrefs".to_vec());
1745        }
1746        if self.unborn {
1747            request.arguments.push(b"unborn".to_vec());
1748        }
1749        for prefix in &self.ref_prefixes {
1750            validate_protocol_v2_token("ls-refs ref-prefix", prefix)?;
1751            request
1752                .arguments
1753                .push(format!("ref-prefix {prefix}").into_bytes());
1754        }
1755        Ok(request)
1756    }
1757}
1758
1759impl ProtocolV2FetchRequest {
1760    pub fn from_command_request(
1761        format: ObjectFormat,
1762        request: &ProtocolV2CommandRequest,
1763    ) -> Result<Self> {
1764        if request.command != "fetch" {
1765            return Err(GitError::InvalidFormat(format!(
1766                "expected fetch command, got {}",
1767                request.command
1768            )));
1769        }
1770        let mut out = Self::default();
1771        for argument in &request.arguments {
1772            let text = std::str::from_utf8(argument)
1773                .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
1774            match text {
1775                "thin-pack" => out.thin_pack = true,
1776                "no-progress" => out.no_progress = true,
1777                "include-tag" => out.include_tag = true,
1778                "ofs-delta" => out.ofs_delta = true,
1779                "sideband-all" => out.sideband_all = true,
1780                "wait-for-done" => out.wait_for_done = true,
1781                "deepen-relative" => out.deepen_relative = true,
1782                "done" => out.done = true,
1783                value if value.starts_with("want ") => {
1784                    out.wants
1785                        .push(parse_oid_argument(format, "fetch want", value, "want ")?);
1786                }
1787                value if value.starts_with("want-ref ") => {
1788                    let name = value
1789                        .strip_prefix("want-ref ")
1790                        .ok_or_else(|| GitError::InvalidFormat("invalid fetch want-ref".into()))?;
1791                    validate_protocol_v2_token("fetch want-ref", name)?;
1792                    out.want_refs.push(name.to_string());
1793                }
1794                value if value.starts_with("have ") => {
1795                    out.haves
1796                        .push(parse_oid_argument(format, "fetch have", value, "have ")?);
1797                }
1798                value if value.starts_with("shallow ") => {
1799                    out.shallow.push(parse_oid_argument(
1800                        format,
1801                        "fetch shallow",
1802                        value,
1803                        "shallow ",
1804                    )?);
1805                }
1806                value if value.starts_with("deepen ") => {
1807                    if out.deepen.is_some() {
1808                        return Err(GitError::InvalidFormat(
1809                            "fetch request has duplicate deepen".into(),
1810                        ));
1811                    }
1812                    out.deepen = Some(parse_u32_argument("fetch deepen", value, "deepen ")?);
1813                }
1814                value if value.starts_with("deepen-since ") => {
1815                    if out.deepen_since.is_some() {
1816                        return Err(GitError::InvalidFormat(
1817                            "fetch request has duplicate deepen-since".into(),
1818                        ));
1819                    }
1820                    out.deepen_since = Some(parse_u64_argument(
1821                        "fetch deepen-since",
1822                        value,
1823                        "deepen-since ",
1824                    )?);
1825                }
1826                value if value.starts_with("deepen-not ") => {
1827                    let name = value.strip_prefix("deepen-not ").ok_or_else(|| {
1828                        GitError::InvalidFormat("invalid fetch deepen-not".into())
1829                    })?;
1830                    validate_protocol_v2_token("fetch deepen-not", name)?;
1831                    out.deepen_not.push(name.to_string());
1832                }
1833                value if value.starts_with("filter ") => {
1834                    if out.filter.is_some() {
1835                        return Err(GitError::InvalidFormat(
1836                            "fetch request has duplicate filter".into(),
1837                        ));
1838                    }
1839                    let filter = value
1840                        .strip_prefix("filter ")
1841                        .ok_or_else(|| GitError::InvalidFormat("invalid fetch filter".into()))?;
1842                    validate_protocol_v2_token("fetch filter", filter)?;
1843                    out.filter = Some(filter.to_string());
1844                }
1845                value if value.starts_with("packfile-uris ") => {
1846                    if out.packfile_uris.is_some() {
1847                        return Err(GitError::InvalidFormat(
1848                            "fetch request has duplicate packfile-uris".into(),
1849                        ));
1850                    }
1851                    let protocols = value.strip_prefix("packfile-uris ").ok_or_else(|| {
1852                        GitError::InvalidFormat("invalid fetch packfile-uris".into())
1853                    })?;
1854                    validate_protocol_v2_token("fetch packfile-uris", protocols)?;
1855                    out.packfile_uris = Some(protocols.to_string());
1856                }
1857                other => {
1858                    return Err(GitError::InvalidFormat(format!(
1859                        "unsupported fetch argument {other}"
1860                    )));
1861                }
1862            }
1863        }
1864        Ok(out)
1865    }
1866
1867    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
1868        let mut request = ProtocolV2CommandRequest::new("fetch")?;
1869        for oid in &self.wants {
1870            request.arguments.push(format!("want {oid}").into_bytes());
1871        }
1872        for name in &self.want_refs {
1873            validate_protocol_v2_token("fetch want-ref", name)?;
1874            request
1875                .arguments
1876                .push(format!("want-ref {name}").into_bytes());
1877        }
1878        for oid in &self.haves {
1879            request.arguments.push(format!("have {oid}").into_bytes());
1880        }
1881        for oid in &self.shallow {
1882            request
1883                .arguments
1884                .push(format!("shallow {oid}").into_bytes());
1885        }
1886        if let Some(deepen) = self.deepen {
1887            if deepen == 0 {
1888                return Err(GitError::InvalidFormat(
1889                    "fetch deepen must be positive".into(),
1890                ));
1891            }
1892            request
1893                .arguments
1894                .push(format!("deepen {deepen}").into_bytes());
1895        }
1896        if let Some(deepen_since) = self.deepen_since {
1897            request
1898                .arguments
1899                .push(format!("deepen-since {deepen_since}").into_bytes());
1900        }
1901        for name in &self.deepen_not {
1902            validate_protocol_v2_token("fetch deepen-not", name)?;
1903            request
1904                .arguments
1905                .push(format!("deepen-not {name}").into_bytes());
1906        }
1907        if self.deepen_relative {
1908            request.arguments.push(b"deepen-relative".to_vec());
1909        }
1910        if let Some(filter) = &self.filter {
1911            validate_protocol_v2_token("fetch filter", filter)?;
1912            request
1913                .arguments
1914                .push(format!("filter {filter}").into_bytes());
1915        }
1916        if let Some(protocols) = &self.packfile_uris {
1917            validate_protocol_v2_token("fetch packfile-uris", protocols)?;
1918            request
1919                .arguments
1920                .push(format!("packfile-uris {protocols}").into_bytes());
1921        }
1922        if self.thin_pack {
1923            request.arguments.push(b"thin-pack".to_vec());
1924        }
1925        if self.no_progress {
1926            request.arguments.push(b"no-progress".to_vec());
1927        }
1928        if self.include_tag {
1929            request.arguments.push(b"include-tag".to_vec());
1930        }
1931        if self.ofs_delta {
1932            request.arguments.push(b"ofs-delta".to_vec());
1933        }
1934        if self.sideband_all {
1935            request.arguments.push(b"sideband-all".to_vec());
1936        }
1937        if self.wait_for_done {
1938            request.arguments.push(b"wait-for-done".to_vec());
1939        }
1940        if self.done {
1941            request.arguments.push(b"done".to_vec());
1942        }
1943        Ok(request)
1944    }
1945}
1946
1947impl ProtocolV2ObjectInfoRequest {
1948    pub fn from_command_request(
1949        format: ObjectFormat,
1950        request: &ProtocolV2CommandRequest,
1951    ) -> Result<Self> {
1952        if request.command != "object-info" {
1953            return Err(GitError::InvalidFormat(format!(
1954                "expected object-info command, got {}",
1955                request.command
1956            )));
1957        }
1958        let mut out = Self::default();
1959        for argument in &request.arguments {
1960            let text = parse_protocol_v2_line_text("object-info request argument", argument)?;
1961            if text == "size" {
1962                if out.size {
1963                    return Err(GitError::InvalidFormat(
1964                        "object-info request has duplicate size argument".into(),
1965                    ));
1966                }
1967                out.size = true;
1968            } else if text.starts_with("oid ") {
1969                out.oids
1970                    .push(parse_oid_argument(format, "object-info oid", text, "oid ")?);
1971            } else {
1972                return Err(GitError::InvalidFormat(format!(
1973                    "unsupported object-info request argument {text}"
1974                )));
1975            }
1976        }
1977        if !out.size {
1978            return Err(GitError::InvalidFormat(
1979                "object-info request is missing size argument".into(),
1980            ));
1981        }
1982        if out.oids.is_empty() {
1983            return Err(GitError::InvalidFormat(
1984                "object-info request is missing object ids".into(),
1985            ));
1986        }
1987        Ok(out)
1988    }
1989
1990    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
1991        if !self.size {
1992            return Err(GitError::InvalidFormat(
1993                "object-info request is missing size argument".into(),
1994            ));
1995        }
1996        if self.oids.is_empty() {
1997            return Err(GitError::InvalidFormat(
1998                "object-info request is missing object ids".into(),
1999            ));
2000        }
2001        let mut request = ProtocolV2CommandRequest::new("object-info")?;
2002        request.arguments.push(b"size".to_vec());
2003        for oid in &self.oids {
2004            request.arguments.push(format!("oid {oid}").into_bytes());
2005        }
2006        Ok(request)
2007    }
2008}
2009
2010pub fn parse_protocol_v2_advertisement(frames: &[PktLineFrame]) -> Result<TransportHandshake> {
2011    let Some((first, rest)) = frames.split_first() else {
2012        return Err(GitError::InvalidFormat(
2013            "protocol v2 advertisement is empty".into(),
2014        ));
2015    };
2016    match first {
2017        PktLineFrame::Data(payload) if trim_trailing_lf(payload) == b"version 2" => {}
2018        PktLineFrame::Data(_) => {
2019            return Err(GitError::InvalidFormat(
2020                "protocol v2 advertisement missing version line".into(),
2021            ));
2022        }
2023        _ => {
2024            return Err(GitError::InvalidFormat(
2025                "protocol v2 advertisement must start with a data line".into(),
2026            ));
2027        }
2028    }
2029
2030    let mut capabilities = Vec::new();
2031    let mut saw_flush = false;
2032    for (idx, frame) in rest.iter().enumerate() {
2033        match frame {
2034            PktLineFrame::Data(payload) => {
2035                if saw_flush {
2036                    return Err(GitError::InvalidFormat(
2037                        "protocol v2 advertisement has data after flush".into(),
2038                    ));
2039                }
2040                capabilities.push(parse_protocol_v2_capability_line(payload)?);
2041            }
2042            PktLineFrame::Flush => {
2043                saw_flush = true;
2044                if idx + 1 != rest.len() {
2045                    return Err(GitError::InvalidFormat(
2046                        "protocol v2 advertisement has frames after flush".into(),
2047                    ));
2048                }
2049            }
2050            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
2051                return Err(GitError::InvalidFormat(
2052                    "protocol v2 advertisement contains a non-flush control packet".into(),
2053                ));
2054            }
2055        }
2056    }
2057    if !saw_flush {
2058        return Err(GitError::InvalidFormat(
2059            "protocol v2 advertisement missing flush".into(),
2060        ));
2061    }
2062
2063    Ok(TransportHandshake {
2064        protocol: ProtocolVersion::V2,
2065        capabilities,
2066    })
2067}
2068
2069pub fn encode_protocol_v2_advertisement(
2070    handshake: &TransportHandshake,
2071) -> Result<Vec<PktLineFrame>> {
2072    if handshake.protocol != ProtocolVersion::V2 {
2073        return Err(GitError::InvalidFormat(
2074            "protocol v2 advertisement requires a v2 handshake".into(),
2075        ));
2076    }
2077    let mut frames = vec![PktLineFrame::data(line_from_str("version 2"))?];
2078    for capability in &handshake.capabilities {
2079        frames.push(PktLineFrame::data(line(encode_protocol_v2_capability(
2080            capability,
2081        )?))?);
2082    }
2083    frames.push(PktLineFrame::Flush);
2084    Ok(frames)
2085}
2086
2087pub fn read_protocol_v2_advertisement(reader: &mut impl Read) -> Result<TransportHandshake> {
2088    let frames = read_pkt_line_frames_until_flush(reader)?;
2089    parse_protocol_v2_advertisement(&frames)
2090}
2091
2092pub fn write_protocol_v2_advertisement(
2093    writer: &mut impl Write,
2094    handshake: &TransportHandshake,
2095) -> Result<()> {
2096    if handshake.protocol != ProtocolVersion::V2 {
2097        return Err(GitError::InvalidFormat(
2098            "protocol v2 advertisement requires a v2 handshake".into(),
2099        ));
2100    }
2101    write_pkt_line_payload(writer, b"version 2\n")?;
2102    for capability in &handshake.capabilities {
2103        write_pkt_line_payload(writer, &line(encode_protocol_v2_capability(capability)?))?;
2104    }
2105    writer.write_all(b"0000")?;
2106    Ok(())
2107}
2108
2109pub fn parse_protocol_v2_command_request(
2110    frames: &[PktLineFrame],
2111) -> Result<ProtocolV2CommandRequest> {
2112    let Some((first, rest)) = frames.split_first() else {
2113        return Err(GitError::InvalidFormat(
2114            "protocol v2 command request is empty".into(),
2115        ));
2116    };
2117    let command = match first {
2118        PktLineFrame::Data(payload) => parse_protocol_v2_command_line(payload)?,
2119        _ => {
2120            return Err(GitError::InvalidFormat(
2121                "protocol v2 command request must start with a command line".into(),
2122            ));
2123        }
2124    };
2125
2126    let mut capabilities = Vec::new();
2127    let mut arguments = Vec::new();
2128    let mut in_arguments = false;
2129    let mut saw_flush = false;
2130    for (idx, frame) in rest.iter().enumerate() {
2131        match frame {
2132            PktLineFrame::Data(payload) if !in_arguments => {
2133                if saw_flush {
2134                    return Err(GitError::InvalidFormat(
2135                        "protocol v2 command request has data after flush".into(),
2136                    ));
2137                }
2138                capabilities.push(parse_protocol_v2_capability_line(payload)?);
2139            }
2140            PktLineFrame::Data(payload) => {
2141                if saw_flush {
2142                    return Err(GitError::InvalidFormat(
2143                        "protocol v2 command request has data after flush".into(),
2144                    ));
2145                }
2146                let argument = trim_trailing_lf(payload);
2147                if argument.is_empty() {
2148                    return Err(GitError::InvalidFormat(
2149                        "protocol v2 command argument is empty".into(),
2150                    ));
2151                }
2152                if argument
2153                    .iter()
2154                    .any(|byte| matches!(*byte, b'\n' | b'\r' | 0))
2155                {
2156                    return Err(GitError::InvalidFormat(
2157                        "protocol v2 command argument contains a delimiter byte".into(),
2158                    ));
2159                }
2160                arguments.push(argument.to_vec());
2161            }
2162            PktLineFrame::Delimiter => {
2163                if in_arguments {
2164                    return Err(GitError::InvalidFormat(
2165                        "protocol v2 command request has duplicate delimiter".into(),
2166                    ));
2167                }
2168                if saw_flush {
2169                    return Err(GitError::InvalidFormat(
2170                        "protocol v2 command request has delimiter after flush".into(),
2171                    ));
2172                }
2173                in_arguments = true;
2174            }
2175            PktLineFrame::Flush => {
2176                saw_flush = true;
2177                if idx + 1 != rest.len() {
2178                    return Err(GitError::InvalidFormat(
2179                        "protocol v2 command request has frames after flush".into(),
2180                    ));
2181                }
2182            }
2183            PktLineFrame::ResponseEnd => {
2184                return Err(GitError::InvalidFormat(
2185                    "protocol v2 command request contains response-end".into(),
2186                ));
2187            }
2188        }
2189    }
2190    if !saw_flush {
2191        return Err(GitError::InvalidFormat(
2192            "protocol v2 command request missing flush".into(),
2193        ));
2194    }
2195
2196    Ok(ProtocolV2CommandRequest {
2197        command,
2198        capabilities,
2199        arguments,
2200    })
2201}
2202
2203pub fn encode_protocol_v2_command_request(
2204    request: &ProtocolV2CommandRequest,
2205) -> Result<Vec<PktLineFrame>> {
2206    validate_capability_name(&request.command)?;
2207    let mut frames = Vec::new();
2208    frames.push(PktLineFrame::data(line_from_str(&format!(
2209        "command={}",
2210        request.command
2211    )))?);
2212    for capability in &request.capabilities {
2213        frames.push(PktLineFrame::data(line(encode_protocol_v2_capability(
2214            capability,
2215        )?))?);
2216    }
2217    if !request.arguments.is_empty() {
2218        frames.push(PktLineFrame::Delimiter);
2219        for argument in &request.arguments {
2220            validate_protocol_v2_argument(argument)?;
2221            let mut payload = argument.clone();
2222            payload.push(b'\n');
2223            frames.push(PktLineFrame::data(payload)?);
2224        }
2225    }
2226    frames.push(PktLineFrame::Flush);
2227    Ok(frames)
2228}
2229
2230pub fn parse_protocol_v2_request(frames: &[PktLineFrame]) -> Result<ProtocolV2Request> {
2231    if matches!(frames, [PktLineFrame::Flush]) {
2232        return Ok(ProtocolV2Request::Done);
2233    }
2234    parse_protocol_v2_command_request(frames).map(ProtocolV2Request::Command)
2235}
2236
2237pub fn encode_protocol_v2_request(request: &ProtocolV2Request) -> Result<Vec<PktLineFrame>> {
2238    match request {
2239        ProtocolV2Request::Command(command) => encode_protocol_v2_command_request(command),
2240        ProtocolV2Request::Done => Ok(vec![PktLineFrame::Flush]),
2241    }
2242}
2243
2244pub fn read_protocol_v2_request(reader: &mut impl Read) -> Result<ProtocolV2Request> {
2245    let frames = read_pkt_line_frames_until_flush(reader)?;
2246    parse_protocol_v2_request(&frames)
2247}
2248
2249pub fn write_protocol_v2_request(
2250    writer: &mut impl Write,
2251    request: &ProtocolV2Request,
2252) -> Result<()> {
2253    match request {
2254        ProtocolV2Request::Command(command) => write_protocol_v2_command_request(writer, command),
2255        ProtocolV2Request::Done => {
2256            writer.write_all(b"0000")?;
2257            Ok(())
2258        }
2259    }
2260}
2261
2262pub fn read_protocol_v2_command_request(
2263    reader: &mut impl Read,
2264) -> Result<ProtocolV2CommandRequest> {
2265    let frames = read_pkt_line_frames_until_flush(reader)?;
2266    parse_protocol_v2_command_request(&frames)
2267}
2268
2269pub fn write_protocol_v2_command_request(
2270    writer: &mut impl Write,
2271    request: &ProtocolV2CommandRequest,
2272) -> Result<()> {
2273    validate_capability_name(&request.command)?;
2274    write_pkt_line_payload(
2275        writer,
2276        &line_from_str(&format!("command={}", request.command)),
2277    )?;
2278    for capability in &request.capabilities {
2279        write_pkt_line_payload(writer, &line(encode_protocol_v2_capability(capability)?))?;
2280    }
2281    if !request.arguments.is_empty() {
2282        write_pkt_line_frame(writer, &PktLineFrame::Delimiter)?;
2283        for argument in &request.arguments {
2284            validate_protocol_v2_argument(argument)?;
2285            let mut payload = argument.clone();
2286            payload.push(b'\n');
2287            write_pkt_line_payload(writer, &payload)?;
2288        }
2289    }
2290    writer.write_all(b"0000")?;
2291    Ok(())
2292}
2293
2294pub fn read_protocol_v2_ls_refs_request(reader: &mut impl Read) -> Result<ProtocolV2LsRefsRequest> {
2295    let request = read_protocol_v2_command_request(reader)?;
2296    ProtocolV2LsRefsRequest::from_command_request(&request)
2297}
2298
2299pub fn write_protocol_v2_ls_refs_request(
2300    writer: &mut impl Write,
2301    request: &ProtocolV2LsRefsRequest,
2302) -> Result<()> {
2303    let command = request.to_command_request()?;
2304    write_protocol_v2_command_request(writer, &command)
2305}
2306
2307pub fn parse_protocol_v2_ls_refs_response(
2308    format: ObjectFormat,
2309    frames: &[PktLineFrame],
2310) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2311    let mut records = Vec::new();
2312    let mut saw_flush = false;
2313    for (idx, frame) in frames.iter().enumerate() {
2314        match frame {
2315            PktLineFrame::Data(payload) => {
2316                if saw_flush {
2317                    return Err(GitError::InvalidFormat(
2318                        "ls-refs response has data after flush".into(),
2319                    ));
2320                }
2321                records.push(parse_protocol_v2_ls_refs_line(format, payload)?);
2322            }
2323            PktLineFrame::Flush => {
2324                saw_flush = true;
2325                if !flush_terminates_protocol_v2_response(frames, idx) {
2326                    return Err(GitError::InvalidFormat(
2327                        "ls-refs response has frames after flush".into(),
2328                    ));
2329                }
2330            }
2331            PktLineFrame::ResponseEnd if saw_flush && idx + 1 == frames.len() => {}
2332            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
2333                return Err(GitError::InvalidFormat(
2334                    "ls-refs response contains a non-flush control packet".into(),
2335                ));
2336            }
2337        }
2338    }
2339    if !saw_flush {
2340        return Err(GitError::InvalidFormat(
2341            "ls-refs response missing flush".into(),
2342        ));
2343    }
2344    Ok(records)
2345}
2346
2347pub fn encode_protocol_v2_ls_refs_response(
2348    records: &[ProtocolV2LsRefsRecord],
2349) -> Result<Vec<PktLineFrame>> {
2350    let mut frames = Vec::new();
2351    for record in records {
2352        frames.push(PktLineFrame::data(line_from_str(
2353            &format_protocol_v2_ls_refs_record(record)?,
2354        ))?);
2355    }
2356    frames.push(PktLineFrame::Flush);
2357    Ok(frames)
2358}
2359
2360pub fn read_protocol_v2_ls_refs_response(
2361    format: ObjectFormat,
2362    reader: &mut impl Read,
2363) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2364    let frames = read_pkt_line_frames_until_flush(reader)?;
2365    parse_protocol_v2_ls_refs_response(format, &frames)
2366}
2367
2368pub fn write_protocol_v2_ls_refs_response(
2369    writer: &mut impl Write,
2370    records: &[ProtocolV2LsRefsRecord],
2371) -> Result<()> {
2372    for record in records {
2373        write_pkt_line_payload(
2374            writer,
2375            &line_from_str(&format_protocol_v2_ls_refs_record(record)?),
2376        )?;
2377    }
2378    writer.write_all(b"0000")?;
2379    Ok(())
2380}
2381
2382pub fn read_protocol_v2_ls_refs_response_until_response_end(
2383    format: ObjectFormat,
2384    reader: &mut impl Read,
2385) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2386    let frames = read_pkt_line_frames_until_response_end(reader)?;
2387    parse_protocol_v2_ls_refs_response(format, &frames)
2388}
2389
2390pub fn write_protocol_v2_ls_refs_response_with_response_end(
2391    writer: &mut impl Write,
2392    records: &[ProtocolV2LsRefsRecord],
2393) -> Result<()> {
2394    write_protocol_v2_ls_refs_response(writer, records)?;
2395    writer.write_all(b"0002")?;
2396    Ok(())
2397}
2398
2399pub fn exchange_protocol_v2_ls_refs(
2400    format: ObjectFormat,
2401    reader: &mut impl Read,
2402    writer: &mut impl Write,
2403    request: &ProtocolV2LsRefsRequest,
2404) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2405    write_protocol_v2_ls_refs_request(writer, request)?;
2406    writer.flush()?;
2407    read_protocol_v2_ls_refs_response(format, reader)
2408}
2409
2410/// Bridge a parsed protocol v2 `ls-refs` response into the shared
2411/// [`RefAdvertisementSet`]/[`RefAdvertisement`] types used by the v0/v1 codecs,
2412/// so callers can drive v2 clone/fetch through the same ref-advertisement
2413/// machinery.
2414///
2415/// Each [`ProtocolV2LsRefsRecord::Ref`] becomes a [`RefAdvertisement`]. A
2416/// `peeled:<oid>` attribute is emitted as an additional `<peeled-oid>
2417/// <name>^{}` advertisement, matching the v0/v1 peeled-tag convention.
2418/// `symref-target:<target>` attributes are collected as `symref=<name>:<target>`
2419/// capabilities on the first advertised ref, mirroring how the upload-pack v0/v1
2420/// advertisement carries symrefs. [`ProtocolV2LsRefsRecord::Unborn`] records have
2421/// no object id, so they cannot be represented as a [`RefAdvertisement`]; an
2422/// unborn record carrying a `symref-target` is preserved as a `symref` capability
2423/// while otherwise being skipped. The returned set always reports
2424/// [`ProtocolVersion::V2`].
2425pub fn protocol_v2_ls_refs_records_to_ref_advertisement_set(
2426    records: &[ProtocolV2LsRefsRecord],
2427) -> Result<RefAdvertisementSet> {
2428    let mut refs: Vec<RefAdvertisement> = Vec::new();
2429    let mut symrefs: Vec<Capability> = Vec::new();
2430    for record in records {
2431        match record {
2432            ProtocolV2LsRefsRecord::Ref(reference) => {
2433                validate_protocol_v2_token("ls-refs ref name", &reference.name)?;
2434                refs.push(RefAdvertisement {
2435                    oid: reference.oid,
2436                    name: reference.name.clone(),
2437                    capabilities: Vec::new(),
2438                });
2439                if let Some(peeled) = &reference.peeled {
2440                    refs.push(RefAdvertisement {
2441                        oid: peeled.clone(),
2442                        name: format!("{}^{{}}", reference.name),
2443                        capabilities: Vec::new(),
2444                    });
2445                }
2446                if let Some(target) = &reference.symref_target {
2447                    symrefs.push(protocol_v2_symref_capability(&reference.name, target)?);
2448                }
2449            }
2450            ProtocolV2LsRefsRecord::Unborn {
2451                name,
2452                symref_target,
2453                ..
2454            } => {
2455                validate_protocol_v2_token("ls-refs ref name", name)?;
2456                if let Some(target) = symref_target {
2457                    symrefs.push(protocol_v2_symref_capability(name, target)?);
2458                }
2459            }
2460        }
2461    }
2462    if !symrefs.is_empty() {
2463        if let Some(first) = refs.first_mut() {
2464            first.capabilities = symrefs;
2465        } else {
2466            return Err(GitError::InvalidFormat(
2467                "ls-refs response advertised symrefs without any concrete refs".into(),
2468            ));
2469        }
2470    }
2471    Ok(RefAdvertisementSet {
2472        protocol: ProtocolVersion::V2,
2473        refs,
2474        shallow: Vec::new(),
2475    })
2476}
2477
2478/// Parse a protocol v2 `ls-refs` response and bridge it into the shared
2479/// [`RefAdvertisementSet`] type. Convenience wrapper combining
2480/// [`parse_protocol_v2_ls_refs_response`] and
2481/// [`protocol_v2_ls_refs_records_to_ref_advertisement_set`].
2482pub fn parse_protocol_v2_ls_refs_response_as_ref_advertisement_set(
2483    format: ObjectFormat,
2484    frames: &[PktLineFrame],
2485) -> Result<RefAdvertisementSet> {
2486    let records = parse_protocol_v2_ls_refs_response(format, frames)?;
2487    protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
2488}
2489
2490/// Read a protocol v2 `ls-refs` response from `reader` and bridge it into the
2491/// shared [`RefAdvertisementSet`] type.
2492pub fn read_protocol_v2_ls_refs_response_as_ref_advertisement_set(
2493    format: ObjectFormat,
2494    reader: &mut impl Read,
2495) -> Result<RefAdvertisementSet> {
2496    let records = read_protocol_v2_ls_refs_response(format, reader)?;
2497    protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
2498}
2499
2500fn protocol_v2_symref_capability(name: &str, target: &str) -> Result<Capability> {
2501    validate_protocol_v2_token("ls-refs symref-target", target)?;
2502    Ok(Capability {
2503        name: "symref".into(),
2504        value: Some(format!("{name}:{target}")),
2505    })
2506}
2507
2508pub fn read_protocol_v2_fetch_request(
2509    format: ObjectFormat,
2510    reader: &mut impl Read,
2511) -> Result<ProtocolV2FetchRequest> {
2512    let request = read_protocol_v2_command_request(reader)?;
2513    ProtocolV2FetchRequest::from_command_request(format, &request)
2514}
2515
2516pub fn write_protocol_v2_fetch_request(
2517    writer: &mut impl Write,
2518    request: &ProtocolV2FetchRequest,
2519) -> Result<()> {
2520    let command = request.to_command_request()?;
2521    write_protocol_v2_command_request(writer, &command)
2522}
2523
2524pub fn read_protocol_v2_object_info_request(
2525    format: ObjectFormat,
2526    reader: &mut impl Read,
2527) -> Result<ProtocolV2ObjectInfoRequest> {
2528    let request = read_protocol_v2_command_request(reader)?;
2529    ProtocolV2ObjectInfoRequest::from_command_request(format, &request)
2530}
2531
2532pub fn write_protocol_v2_object_info_request(
2533    writer: &mut impl Write,
2534    request: &ProtocolV2ObjectInfoRequest,
2535) -> Result<()> {
2536    let command = request.to_command_request()?;
2537    write_protocol_v2_command_request(writer, &command)
2538}
2539
2540pub fn parse_protocol_v2_fetch_response(
2541    format: ObjectFormat,
2542    frames: &[PktLineFrame],
2543) -> Result<Vec<ProtocolV2FetchResponseSection>> {
2544    let mut sections = Vec::new();
2545    let mut current: Option<(String, Vec<Vec<u8>>)> = None;
2546    let mut saw_flush = false;
2547    for (idx, frame) in frames.iter().enumerate() {
2548        match frame {
2549            PktLineFrame::Data(payload) => {
2550                if saw_flush {
2551                    return Err(GitError::InvalidFormat(
2552                        "fetch response has data after flush".into(),
2553                    ));
2554                }
2555                if let Some((_name, lines)) = &mut current {
2556                    lines.push(payload.clone());
2557                } else {
2558                    let name = parse_fetch_section_header(payload)?;
2559                    current = Some((name, Vec::new()));
2560                }
2561            }
2562            PktLineFrame::Delimiter => {
2563                if saw_flush {
2564                    return Err(GitError::InvalidFormat(
2565                        "fetch response has delimiter after flush".into(),
2566                    ));
2567                }
2568                let Some((name, lines)) = current.take() else {
2569                    return Err(GitError::InvalidFormat(
2570                        "fetch response has delimiter before section".into(),
2571                    ));
2572                };
2573                sections.push(parse_fetch_section(format, name, lines)?);
2574            }
2575            PktLineFrame::Flush => {
2576                saw_flush = true;
2577                if !flush_terminates_protocol_v2_response(frames, idx) {
2578                    return Err(GitError::InvalidFormat(
2579                        "fetch response has frames after flush".into(),
2580                    ));
2581                }
2582                if let Some((name, lines)) = current.take() {
2583                    sections.push(parse_fetch_section(format, name, lines)?);
2584                }
2585            }
2586            PktLineFrame::ResponseEnd if saw_flush && idx + 1 == frames.len() => {}
2587            PktLineFrame::ResponseEnd => {
2588                return Err(GitError::InvalidFormat(
2589                    "fetch response contains response-end".into(),
2590                ));
2591            }
2592        }
2593    }
2594    if !saw_flush {
2595        return Err(GitError::InvalidFormat(
2596            "fetch response missing flush".into(),
2597        ));
2598    }
2599    Ok(sections)
2600}
2601
2602pub fn encode_protocol_v2_fetch_response(
2603    sections: &[ProtocolV2FetchResponseSection],
2604) -> Result<Vec<PktLineFrame>> {
2605    let mut frames = Vec::new();
2606    for (idx, section) in sections.iter().enumerate() {
2607        if idx != 0 {
2608            frames.push(PktLineFrame::Delimiter);
2609        }
2610        frames.push(PktLineFrame::data(line_from_str(
2611            protocol_v2_fetch_section_name(section),
2612        ))?);
2613        for line in format_protocol_v2_fetch_section_lines(section)? {
2614            frames.push(PktLineFrame::data(line)?);
2615        }
2616    }
2617    frames.push(PktLineFrame::Flush);
2618    Ok(frames)
2619}
2620
2621pub fn parse_protocol_v2_fetch_sideband_all_response(
2622    format: ObjectFormat,
2623    frames: &[PktLineFrame],
2624) -> Result<ProtocolV2FetchSidebandAllResponse> {
2625    let mut demuxed = Vec::new();
2626    let mut progress = Vec::new();
2627    let mut in_packfile = false;
2628    for frame in frames {
2629        match frame {
2630            PktLineFrame::Data(payload) if in_packfile => {
2631                demuxed.push(PktLineFrame::Data(payload.clone()));
2632            }
2633            PktLineFrame::Data(payload) => {
2634                let packet = parse_sideband_packet(payload)?;
2635                match packet.channel {
2636                    SideBandChannel::Data => {
2637                        if trim_trailing_lf(&packet.data) == b"packfile" {
2638                            in_packfile = true;
2639                        }
2640                        demuxed.push(PktLineFrame::Data(packet.data));
2641                    }
2642                    SideBandChannel::Progress => progress.push(packet.data),
2643                    SideBandChannel::Fatal => {
2644                        let message = String::from_utf8_lossy(&packet.data).into_owned();
2645                        return Err(GitError::InvalidFormat(format!(
2646                            "sideband fatal: {message}"
2647                        )));
2648                    }
2649                }
2650            }
2651            PktLineFrame::Delimiter => {
2652                in_packfile = false;
2653                demuxed.push(PktLineFrame::Delimiter);
2654            }
2655            PktLineFrame::Flush => {
2656                in_packfile = false;
2657                demuxed.push(PktLineFrame::Flush);
2658            }
2659            PktLineFrame::ResponseEnd => {
2660                in_packfile = false;
2661                demuxed.push(PktLineFrame::ResponseEnd);
2662            }
2663        }
2664    }
2665    Ok(ProtocolV2FetchSidebandAllResponse {
2666        sections: parse_protocol_v2_fetch_response(format, &demuxed)?,
2667        progress,
2668    })
2669}
2670
2671pub fn encode_protocol_v2_fetch_sideband_all_response(
2672    sections: &[ProtocolV2FetchResponseSection],
2673) -> Result<Vec<PktLineFrame>> {
2674    let frames = encode_protocol_v2_fetch_response(sections)?;
2675    let mut encoded = Vec::new();
2676    let mut in_packfile = false;
2677    for frame in frames {
2678        match frame {
2679            PktLineFrame::Data(payload) if in_packfile => {
2680                encoded.push(PktLineFrame::Data(payload));
2681            }
2682            PktLineFrame::Data(payload) => {
2683                if trim_trailing_lf(&payload) == b"packfile" {
2684                    in_packfile = true;
2685                }
2686                encoded.push(PktLineFrame::data(encode_sideband_packet(
2687                    &SideBandPacket {
2688                        channel: SideBandChannel::Data,
2689                        data: payload,
2690                    },
2691                )?)?);
2692            }
2693            PktLineFrame::Delimiter => {
2694                in_packfile = false;
2695                encoded.push(PktLineFrame::Delimiter);
2696            }
2697            PktLineFrame::Flush => {
2698                in_packfile = false;
2699                encoded.push(PktLineFrame::Flush);
2700            }
2701            PktLineFrame::ResponseEnd => {
2702                in_packfile = false;
2703                encoded.push(PktLineFrame::ResponseEnd);
2704            }
2705        }
2706    }
2707    Ok(encoded)
2708}
2709
2710pub fn read_protocol_v2_fetch_response(
2711    format: ObjectFormat,
2712    reader: &mut impl Read,
2713) -> Result<Vec<ProtocolV2FetchResponseSection>> {
2714    let frames = read_pkt_line_frames_until_flush(reader)?;
2715    parse_protocol_v2_fetch_response(format, &frames)
2716}
2717
2718pub fn write_protocol_v2_fetch_response(
2719    writer: &mut impl Write,
2720    sections: &[ProtocolV2FetchResponseSection],
2721) -> Result<()> {
2722    write_protocol_v2_fetch_response_inner(writer, sections, false, false)
2723}
2724
2725pub fn read_protocol_v2_fetch_sideband_all_response(
2726    format: ObjectFormat,
2727    reader: &mut impl Read,
2728) -> Result<ProtocolV2FetchSidebandAllResponse> {
2729    let frames = read_pkt_line_frames_until_flush(reader)?;
2730    parse_protocol_v2_fetch_sideband_all_response(format, &frames)
2731}
2732
2733pub fn write_protocol_v2_fetch_sideband_all_response(
2734    writer: &mut impl Write,
2735    sections: &[ProtocolV2FetchResponseSection],
2736) -> Result<()> {
2737    write_protocol_v2_fetch_response_inner(writer, sections, true, false)
2738}
2739
2740pub fn read_protocol_v2_fetch_response_until_response_end(
2741    format: ObjectFormat,
2742    reader: &mut impl Read,
2743) -> Result<Vec<ProtocolV2FetchResponseSection>> {
2744    let frames = read_pkt_line_frames_until_response_end(reader)?;
2745    parse_protocol_v2_fetch_response(format, &frames)
2746}
2747
2748pub fn write_protocol_v2_fetch_response_with_response_end(
2749    writer: &mut impl Write,
2750    sections: &[ProtocolV2FetchResponseSection],
2751) -> Result<()> {
2752    write_protocol_v2_fetch_response_inner(writer, sections, false, true)
2753}
2754
2755pub fn read_protocol_v2_fetch_sideband_all_response_until_response_end(
2756    format: ObjectFormat,
2757    reader: &mut impl Read,
2758) -> Result<ProtocolV2FetchSidebandAllResponse> {
2759    let frames = read_pkt_line_frames_until_response_end(reader)?;
2760    parse_protocol_v2_fetch_sideband_all_response(format, &frames)
2761}
2762
2763pub fn write_protocol_v2_fetch_sideband_all_response_with_response_end(
2764    writer: &mut impl Write,
2765    sections: &[ProtocolV2FetchResponseSection],
2766) -> Result<()> {
2767    write_protocol_v2_fetch_response_inner(writer, sections, true, true)
2768}
2769
2770fn write_protocol_v2_fetch_response_inner(
2771    writer: &mut impl Write,
2772    sections: &[ProtocolV2FetchResponseSection],
2773    sideband_all: bool,
2774    response_end: bool,
2775) -> Result<()> {
2776    let mut in_packfile = false;
2777    for (idx, section) in sections.iter().enumerate() {
2778        if idx != 0 {
2779            in_packfile = false;
2780            write_pkt_line_frame(writer, &PktLineFrame::Delimiter)?;
2781        }
2782        write_protocol_v2_fetch_payload(
2783            writer,
2784            &line_from_str(protocol_v2_fetch_section_name(section)),
2785            sideband_all,
2786            &mut in_packfile,
2787        )?;
2788        for payload in format_protocol_v2_fetch_section_lines(section)? {
2789            write_protocol_v2_fetch_payload(writer, &payload, sideband_all, &mut in_packfile)?;
2790        }
2791    }
2792    writer.write_all(b"0000")?;
2793    if response_end {
2794        writer.write_all(b"0002")?;
2795    }
2796    Ok(())
2797}
2798
2799fn write_protocol_v2_fetch_payload(
2800    writer: &mut impl Write,
2801    payload: &[u8],
2802    sideband_all: bool,
2803    in_packfile: &mut bool,
2804) -> Result<()> {
2805    if sideband_all && !*in_packfile {
2806        if trim_trailing_lf(payload) == b"packfile" {
2807            *in_packfile = true;
2808        }
2809        write_sideband_payload(writer, SideBandChannel::Data, payload)
2810    } else {
2811        write_pkt_line_payload(writer, payload)
2812    }
2813}
2814
2815pub fn exchange_protocol_v2_fetch(
2816    format: ObjectFormat,
2817    reader: &mut impl Read,
2818    writer: &mut impl Write,
2819    request: &ProtocolV2FetchRequest,
2820) -> Result<Vec<ProtocolV2FetchResponseSection>> {
2821    write_protocol_v2_fetch_request(writer, request)?;
2822    writer.flush()?;
2823    read_protocol_v2_fetch_response(format, reader)
2824}
2825
2826pub fn parse_protocol_v2_object_info_response(
2827    format: ObjectFormat,
2828    frames: &[PktLineFrame],
2829) -> Result<ProtocolV2ObjectInfoResponse> {
2830    let Some((first, rest)) = frames.split_first() else {
2831        return Err(GitError::InvalidFormat(
2832            "object-info response is empty".into(),
2833        ));
2834    };
2835    let PktLineFrame::Data(attrs) = first else {
2836        return Err(GitError::InvalidFormat(
2837            "object-info response must start with attributes".into(),
2838        ));
2839    };
2840    let attrs = parse_protocol_v2_line_text("object-info response attributes", attrs)?;
2841    let mut response = ProtocolV2ObjectInfoResponse::default();
2842    for attr in attrs.split(' ') {
2843        validate_protocol_v2_token("object-info response attribute", attr)?;
2844        match attr {
2845            "size" => {
2846                if response.size {
2847                    return Err(GitError::InvalidFormat(
2848                        "object-info response has duplicate size attribute".into(),
2849                    ));
2850                }
2851                response.size = true;
2852            }
2853            other => {
2854                return Err(GitError::InvalidFormat(format!(
2855                    "unsupported object-info response attribute {other}"
2856                )));
2857            }
2858        }
2859    }
2860    if !response.size {
2861        return Err(GitError::InvalidFormat(
2862            "object-info response is missing size attribute".into(),
2863        ));
2864    }
2865
2866    let mut saw_flush = false;
2867    for (idx, frame) in rest.iter().enumerate() {
2868        match frame {
2869            PktLineFrame::Data(payload) if !saw_flush => {
2870                response
2871                    .records
2872                    .push(parse_protocol_v2_object_info_record(format, payload)?);
2873            }
2874            PktLineFrame::Data(_) => {
2875                return Err(GitError::InvalidFormat(
2876                    "object-info response has data after flush".into(),
2877                ));
2878            }
2879            PktLineFrame::Flush => {
2880                saw_flush = true;
2881                if idx + 1 != rest.len() {
2882                    return Err(GitError::InvalidFormat(
2883                        "object-info response has frames after flush".into(),
2884                    ));
2885                }
2886            }
2887            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
2888                return Err(GitError::InvalidFormat(
2889                    "object-info response contains a non-flush control packet".into(),
2890                ));
2891            }
2892        }
2893    }
2894    if !saw_flush {
2895        return Err(GitError::InvalidFormat(
2896            "object-info response missing flush".into(),
2897        ));
2898    }
2899    Ok(response)
2900}
2901
2902pub fn encode_protocol_v2_object_info_response(
2903    response: &ProtocolV2ObjectInfoResponse,
2904) -> Result<Vec<PktLineFrame>> {
2905    if !response.size {
2906        return Err(GitError::InvalidFormat(
2907            "object-info response is missing size attribute".into(),
2908        ));
2909    }
2910    let mut frames = Vec::new();
2911    frames.push(PktLineFrame::data(line_from_str("size"))?);
2912    for record in &response.records {
2913        frames.push(PktLineFrame::data(line_from_str(&format!(
2914            "{} {}",
2915            record.oid, record.size
2916        )))?);
2917    }
2918    frames.push(PktLineFrame::Flush);
2919    Ok(frames)
2920}
2921
2922pub fn read_protocol_v2_object_info_response(
2923    format: ObjectFormat,
2924    reader: &mut impl Read,
2925) -> Result<ProtocolV2ObjectInfoResponse> {
2926    let frames = read_pkt_line_frames_until_flush(reader)?;
2927    parse_protocol_v2_object_info_response(format, &frames)
2928}
2929
2930pub fn write_protocol_v2_object_info_response(
2931    writer: &mut impl Write,
2932    response: &ProtocolV2ObjectInfoResponse,
2933) -> Result<()> {
2934    if !response.size {
2935        return Err(GitError::InvalidFormat(
2936            "object-info response is missing size attribute".into(),
2937        ));
2938    }
2939    write_pkt_line_payload(writer, b"size\n")?;
2940    for record in &response.records {
2941        write_pkt_line_payload(
2942            writer,
2943            &line_from_str(&format!("{} {}", record.oid, record.size)),
2944        )?;
2945    }
2946    writer.write_all(b"0000")?;
2947    Ok(())
2948}
2949
2950pub fn exchange_protocol_v2_object_info(
2951    format: ObjectFormat,
2952    reader: &mut impl Read,
2953    writer: &mut impl Write,
2954    request: &ProtocolV2ObjectInfoRequest,
2955) -> Result<ProtocolV2ObjectInfoResponse> {
2956    write_protocol_v2_object_info_request(writer, request)?;
2957    writer.flush()?;
2958    read_protocol_v2_object_info_response(format, reader)
2959}
2960
2961pub fn demux_protocol_v2_fetch_packfile(
2962    sections: &[ProtocolV2FetchResponseSection],
2963) -> Result<Option<SideBandDemux>> {
2964    let mut packfile = None;
2965    for section in sections {
2966        if let ProtocolV2FetchResponseSection::Packfile(lines) = section {
2967            if packfile.is_some() {
2968                return Err(GitError::InvalidFormat(
2969                    "fetch response has duplicate packfile sections".into(),
2970                ));
2971            }
2972            packfile = Some(parse_and_demux_sideband_packets(lines)?);
2973        }
2974    }
2975    Ok(packfile)
2976}
2977
2978pub fn protocol_v2_object_format(capabilities: &[Capability]) -> Result<ObjectFormat> {
2979    let mut format = None;
2980    for capability in capabilities {
2981        if capability.name != "object-format" {
2982            continue;
2983        }
2984        if format.is_some() {
2985            return Err(GitError::InvalidFormat(
2986                "protocol v2 has duplicate object-format capabilities".into(),
2987            ));
2988        }
2989        let Some(value) = &capability.value else {
2990            return Err(GitError::InvalidFormat(
2991                "protocol v2 object-format capability is missing a value".into(),
2992            ));
2993        };
2994        format = Some(value.parse::<ObjectFormat>()?);
2995    }
2996    Ok(format.unwrap_or(ObjectFormat::Sha1))
2997}
2998
2999pub fn validate_protocol_v2_command_request_capabilities(
3000    handshake: &TransportHandshake,
3001    request: &ProtocolV2CommandRequest,
3002) -> Result<()> {
3003    if handshake.protocol != ProtocolVersion::V2 {
3004        return Err(GitError::InvalidFormat(
3005            "protocol v2 command validation requires a v2 handshake".into(),
3006        ));
3007    }
3008    let advertised =
3009        protocol_v2_capability(&handshake.capabilities, &request.command).ok_or_else(|| {
3010            GitError::InvalidFormat(format!("unadvertised command {}", request.command))
3011        })?;
3012    if advertised.name.is_empty() {
3013        return Err(GitError::InvalidFormat(
3014            "advertised command capability is empty".into(),
3015        ));
3016    }
3017    parse_protocol_v2_command_options(&request.capabilities)?;
3018
3019    for capability in &request.capabilities {
3020        let advertised = protocol_v2_capability(&handshake.capabilities, &capability.name)
3021            .ok_or_else(|| {
3022                GitError::InvalidFormat(format!(
3023                    "unadvertised protocol v2 capability {}",
3024                    capability.name
3025                ))
3026            })?;
3027        if capability.name == "object-format" {
3028            validate_protocol_v2_object_format_request(advertised, capability)?;
3029        }
3030    }
3031    Ok(())
3032}
3033
3034pub fn parse_protocol_v2_command_options(
3035    capabilities: &[Capability],
3036) -> Result<ProtocolV2CommandOptions> {
3037    let mut out = ProtocolV2CommandOptions::default();
3038    for capability in capabilities {
3039        match capability.name.as_str() {
3040            "agent" => {
3041                if out.agent.is_some() {
3042                    return Err(GitError::InvalidFormat(
3043                        "protocol v2 command has duplicate agent capabilities".into(),
3044                    ));
3045                }
3046                let Some(value) = &capability.value else {
3047                    return Err(GitError::InvalidFormat(
3048                        "protocol v2 agent capability is missing a value".into(),
3049                    ));
3050                };
3051                validate_protocol_v2_capability_value(value)?;
3052                out.agent = Some(value.clone());
3053            }
3054            "object-format" => {
3055                if out.object_format.is_some() {
3056                    return Err(GitError::InvalidFormat(
3057                        "protocol v2 command has duplicate object-format capabilities".into(),
3058                    ));
3059                }
3060                let Some(value) = &capability.value else {
3061                    return Err(GitError::InvalidFormat(
3062                        "protocol v2 object-format capability is missing a value".into(),
3063                    ));
3064                };
3065                out.object_format = Some(value.parse::<ObjectFormat>()?);
3066            }
3067            "server-option" => {
3068                let Some(value) = &capability.value else {
3069                    return Err(GitError::InvalidFormat(
3070                        "protocol v2 server-option capability is missing a value".into(),
3071                    ));
3072                };
3073                validate_protocol_v2_capability_value(value)?;
3074                out.server_options.push(value.clone());
3075            }
3076            _ => out.extra.push(capability.clone()),
3077        }
3078    }
3079    Ok(out)
3080}
3081
3082pub fn encode_protocol_v2_command_options(
3083    options: &ProtocolV2CommandOptions,
3084) -> Result<Vec<Capability>> {
3085    let mut capabilities = Vec::new();
3086    if let Some(agent) = &options.agent {
3087        validate_protocol_v2_capability_value(agent)?;
3088        capabilities.push(Capability {
3089            name: "agent".into(),
3090            value: Some(agent.clone()),
3091        });
3092    }
3093    if let Some(format) = options.object_format {
3094        capabilities.push(Capability {
3095            name: "object-format".into(),
3096            value: Some(format.name().into()),
3097        });
3098    }
3099    for option in &options.server_options {
3100        validate_protocol_v2_capability_value(option)?;
3101        capabilities.push(Capability {
3102            name: "server-option".into(),
3103            value: Some(option.clone()),
3104        });
3105    }
3106    for capability in &options.extra {
3107        if matches!(
3108            capability.name.as_str(),
3109            "agent" | "object-format" | "server-option"
3110        ) {
3111            return Err(GitError::InvalidFormat(format!(
3112                "protocol v2 extra capability duplicates known capability {}",
3113                capability.name
3114            )));
3115        }
3116        encode_protocol_v2_capability(capability)?;
3117        capabilities.push(capability.clone());
3118    }
3119    Ok(capabilities)
3120}
3121
3122pub fn parse_protocol_v2_ls_refs_features(
3123    capabilities: &[Capability],
3124) -> Result<Option<ProtocolV2LsRefsFeatures>> {
3125    let mut ls_refs = None;
3126    for capability in capabilities {
3127        if capability.name != "ls-refs" {
3128            continue;
3129        }
3130        if ls_refs.is_some() {
3131            return Err(GitError::InvalidFormat(
3132                "protocol v2 has duplicate ls-refs capabilities".into(),
3133            ));
3134        }
3135        ls_refs = Some(parse_protocol_v2_ls_refs_feature_value(
3136            capability.value.as_deref(),
3137        )?);
3138    }
3139    Ok(ls_refs)
3140}
3141
3142pub fn encode_protocol_v2_ls_refs_capability(
3143    features: &ProtocolV2LsRefsFeatures,
3144) -> Result<Capability> {
3145    let mut values = Vec::new();
3146    if features.unborn {
3147        values.push("unborn".to_string());
3148    }
3149    for feature in &features.unknown {
3150        validate_protocol_v2_token("ls-refs feature", feature)?;
3151        if feature == "unborn" {
3152            return Err(GitError::InvalidFormat(
3153                "ls-refs unknown features must not duplicate known feature unborn".into(),
3154            ));
3155        }
3156        values.push(feature.clone());
3157    }
3158    Ok(Capability {
3159        name: "ls-refs".into(),
3160        value: (!values.is_empty()).then(|| values.join(" ")),
3161    })
3162}
3163
3164pub fn validate_protocol_v2_ls_refs_request_features(
3165    features: &ProtocolV2LsRefsFeatures,
3166    request: &ProtocolV2LsRefsRequest,
3167) -> Result<()> {
3168    if request.unborn && !features.unborn {
3169        return Err(GitError::InvalidFormat(
3170            "ls-refs request uses unborn without advertised unborn feature".into(),
3171        ));
3172    }
3173    Ok(())
3174}
3175
3176pub fn validate_protocol_v2_ls_refs_command_request(
3177    handshake: &TransportHandshake,
3178    request: &ProtocolV2CommandRequest,
3179) -> Result<ProtocolV2LsRefsRequest> {
3180    validate_protocol_v2_command_request_capabilities(handshake, request)?;
3181    let ls_refs = ProtocolV2LsRefsRequest::from_command_request(request)?;
3182    let features = parse_protocol_v2_ls_refs_features(&handshake.capabilities)?
3183        .ok_or_else(|| GitError::InvalidFormat("ls-refs command was not advertised".into()))?;
3184    validate_protocol_v2_ls_refs_request_features(&features, &ls_refs)?;
3185    Ok(ls_refs)
3186}
3187
3188pub fn parse_protocol_v2_fetch_features(
3189    capabilities: &[Capability],
3190) -> Result<Option<ProtocolV2FetchFeatures>> {
3191    let mut fetch = None;
3192    for capability in capabilities {
3193        if capability.name != "fetch" {
3194            continue;
3195        }
3196        if fetch.is_some() {
3197            return Err(GitError::InvalidFormat(
3198                "protocol v2 has duplicate fetch capabilities".into(),
3199            ));
3200        }
3201        fetch = Some(parse_protocol_v2_fetch_feature_value(
3202            capability.value.as_deref(),
3203        )?);
3204    }
3205    Ok(fetch)
3206}
3207
3208pub fn encode_protocol_v2_fetch_capability(
3209    features: &ProtocolV2FetchFeatures,
3210) -> Result<Capability> {
3211    let mut values = Vec::new();
3212    if features.shallow {
3213        values.push("shallow".to_string());
3214    }
3215    if features.wait_for_done {
3216        values.push("wait-for-done".to_string());
3217    }
3218    if features.filter {
3219        values.push("filter".to_string());
3220    }
3221    if features.ref_in_want {
3222        values.push("ref-in-want".to_string());
3223    }
3224    if features.sideband_all {
3225        values.push("sideband-all".to_string());
3226    }
3227    if features.packfile_uris {
3228        values.push("packfile-uris".to_string());
3229    }
3230    for feature in &features.unknown {
3231        validate_protocol_v2_token("fetch feature", feature)?;
3232        if matches!(
3233            feature.as_str(),
3234            "shallow"
3235                | "wait-for-done"
3236                | "filter"
3237                | "ref-in-want"
3238                | "sideband-all"
3239                | "packfile-uris"
3240        ) {
3241            return Err(GitError::InvalidFormat(format!(
3242                "fetch unknown features must not duplicate known feature {feature}"
3243            )));
3244        }
3245        values.push(feature.clone());
3246    }
3247    Ok(Capability {
3248        name: "fetch".into(),
3249        value: (!values.is_empty()).then(|| values.join(" ")),
3250    })
3251}
3252
3253pub fn validate_protocol_v2_fetch_request_features(
3254    features: &ProtocolV2FetchFeatures,
3255    request: &ProtocolV2FetchRequest,
3256) -> Result<()> {
3257    if !features.shallow
3258        && (!request.shallow.is_empty()
3259            || request.deepen.is_some()
3260            || request.deepen_since.is_some()
3261            || !request.deepen_not.is_empty()
3262            || request.deepen_relative)
3263    {
3264        return Err(GitError::InvalidFormat(
3265            "fetch request uses shallow/deepen arguments without advertised shallow feature".into(),
3266        ));
3267    }
3268    if !features.filter && request.filter.is_some() {
3269        return Err(GitError::InvalidFormat(
3270            "fetch request uses filter without advertised filter feature".into(),
3271        ));
3272    }
3273    if !features.ref_in_want && !request.want_refs.is_empty() {
3274        return Err(GitError::InvalidFormat(
3275            "fetch request uses want-ref without advertised ref-in-want feature".into(),
3276        ));
3277    }
3278    if !features.sideband_all && request.sideband_all {
3279        return Err(GitError::InvalidFormat(
3280            "fetch request uses sideband-all without advertised sideband-all feature".into(),
3281        ));
3282    }
3283    if !features.packfile_uris && request.packfile_uris.is_some() {
3284        return Err(GitError::InvalidFormat(
3285            "fetch request uses packfile-uris without advertised packfile-uris feature".into(),
3286        ));
3287    }
3288    if !features.wait_for_done && request.wait_for_done {
3289        return Err(GitError::InvalidFormat(
3290            "fetch request uses wait-for-done without advertised wait-for-done feature".into(),
3291        ));
3292    }
3293    Ok(())
3294}
3295
3296pub fn validate_protocol_v2_fetch_command_request(
3297    handshake: &TransportHandshake,
3298    format: ObjectFormat,
3299    request: &ProtocolV2CommandRequest,
3300) -> Result<ProtocolV2FetchRequest> {
3301    validate_protocol_v2_command_request_capabilities(handshake, request)?;
3302    let fetch = ProtocolV2FetchRequest::from_command_request(format, request)?;
3303    let features = parse_protocol_v2_fetch_features(&handshake.capabilities)?
3304        .ok_or_else(|| GitError::InvalidFormat("fetch command was not advertised".into()))?;
3305    validate_protocol_v2_fetch_request_features(&features, &fetch)?;
3306    Ok(fetch)
3307}
3308
3309pub fn validate_protocol_v2_object_info_command_request(
3310    handshake: &TransportHandshake,
3311    format: ObjectFormat,
3312    request: &ProtocolV2CommandRequest,
3313) -> Result<ProtocolV2ObjectInfoRequest> {
3314    validate_protocol_v2_command_request_capabilities(handshake, request)?;
3315    let object_info = ProtocolV2ObjectInfoRequest::from_command_request(format, request)?;
3316    protocol_v2_capability(&handshake.capabilities, "object-info")
3317        .ok_or_else(|| GitError::InvalidFormat("object-info command was not advertised".into()))?;
3318    Ok(object_info)
3319}
3320
3321pub fn classify_protocol_v2_command_request(
3322    handshake: &TransportHandshake,
3323    format: ObjectFormat,
3324    request: &ProtocolV2CommandRequest,
3325) -> Result<ProtocolV2Command> {
3326    match request.command.as_str() {
3327        "ls-refs" => validate_protocol_v2_ls_refs_command_request(handshake, request)
3328            .map(ProtocolV2Command::LsRefs),
3329        "fetch" => validate_protocol_v2_fetch_command_request(handshake, format, request)
3330            .map(ProtocolV2Command::Fetch),
3331        "object-info" => {
3332            validate_protocol_v2_object_info_command_request(handshake, format, request)
3333                .map(ProtocolV2Command::ObjectInfo)
3334        }
3335        _ => {
3336            validate_protocol_v2_command_request_capabilities(handshake, request)?;
3337            Ok(ProtocolV2Command::Unknown(request.clone()))
3338        }
3339    }
3340}
3341
3342pub fn classify_protocol_v2_request(
3343    handshake: &TransportHandshake,
3344    format: ObjectFormat,
3345    request: &ProtocolV2Request,
3346) -> Result<ProtocolV2SessionRequest> {
3347    match request {
3348        ProtocolV2Request::Command(command) => {
3349            classify_protocol_v2_command_request(handshake, format, command)
3350                .map(ProtocolV2SessionRequest::Command)
3351        }
3352        ProtocolV2Request::Done => Ok(ProtocolV2SessionRequest::Done),
3353    }
3354}
3355
3356pub fn read_protocol_v2_session_request(
3357    handshake: &TransportHandshake,
3358    format: ObjectFormat,
3359    reader: &mut impl Read,
3360) -> Result<ProtocolV2SessionRequest> {
3361    let request = read_protocol_v2_request(reader)?;
3362    classify_protocol_v2_request(handshake, format, &request)
3363}
3364
3365fn protocol_v2_capability<'a>(
3366    capabilities: &'a [Capability],
3367    name: &str,
3368) -> Option<&'a Capability> {
3369    capabilities
3370        .iter()
3371        .find(|capability| capability.name == name)
3372}
3373
3374fn validate_protocol_v2_object_format_request(
3375    advertised: &Capability,
3376    requested: &Capability,
3377) -> Result<()> {
3378    let Some(advertised) = &advertised.value else {
3379        return Err(GitError::InvalidFormat(
3380            "advertised object-format capability is missing a value".into(),
3381        ));
3382    };
3383    let Some(requested) = &requested.value else {
3384        return Err(GitError::InvalidFormat(
3385            "requested object-format capability is missing a value".into(),
3386        ));
3387    };
3388    if advertised != requested {
3389        return Err(GitError::InvalidFormat(format!(
3390            "requested object-format {requested} does not match advertised {advertised}"
3391        )));
3392    }
3393    Ok(())
3394}
3395
3396fn parse_protocol_v2_ls_refs_feature_value(
3397    value: Option<&str>,
3398) -> Result<ProtocolV2LsRefsFeatures> {
3399    let mut out = ProtocolV2LsRefsFeatures::default();
3400    let Some(value) = value else {
3401        return Ok(out);
3402    };
3403    if value.is_empty() {
3404        return Err(GitError::InvalidFormat(
3405            "protocol v2 ls-refs capability value is empty".into(),
3406        ));
3407    }
3408    for feature in value.split(' ') {
3409        validate_protocol_v2_token("ls-refs feature", feature)?;
3410        match feature {
3411            "unborn" => out.unborn = true,
3412            other => out.unknown.push(other.to_string()),
3413        }
3414    }
3415    Ok(out)
3416}
3417
3418fn parse_protocol_v2_fetch_feature_value(value: Option<&str>) -> Result<ProtocolV2FetchFeatures> {
3419    let mut out = ProtocolV2FetchFeatures::default();
3420    let Some(value) = value else {
3421        return Ok(out);
3422    };
3423    if value.is_empty() {
3424        return Err(GitError::InvalidFormat(
3425            "protocol v2 fetch capability value is empty".into(),
3426        ));
3427    }
3428    for feature in value.split(' ') {
3429        validate_protocol_v2_token("fetch feature", feature)?;
3430        match feature {
3431            "shallow" => out.shallow = true,
3432            "wait-for-done" => out.wait_for_done = true,
3433            "filter" => out.filter = true,
3434            "ref-in-want" => out.ref_in_want = true,
3435            "sideband-all" => out.sideband_all = true,
3436            "packfile-uris" => out.packfile_uris = true,
3437            other => out.unknown.push(other.to_string()),
3438        }
3439    }
3440    Ok(out)
3441}
3442
3443pub fn parse_capabilities(input: &[u8]) -> Result<Vec<Capability>> {
3444    let input = trim_trailing_lf(input);
3445    if input.is_empty() {
3446        return Ok(Vec::new());
3447    }
3448    let text =
3449        std::str::from_utf8(input).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
3450    text.split(' ')
3451        .map(parse_capability_token)
3452        .collect::<Result<Vec<_>>>()
3453}
3454
3455pub fn encode_capabilities(capabilities: &[Capability]) -> Result<Vec<u8>> {
3456    let mut out = Vec::new();
3457    for (idx, capability) in capabilities.iter().enumerate() {
3458        validate_capability_field("capability name", &capability.name)?;
3459        if idx != 0 {
3460            out.push(b' ');
3461        }
3462        out.extend_from_slice(capability.name.as_bytes());
3463        if let Some(value) = &capability.value {
3464            validate_capability_field("capability value", value)?;
3465            out.push(b'=');
3466            out.extend_from_slice(value.as_bytes());
3467        }
3468    }
3469    Ok(out)
3470}
3471
3472pub fn parse_ref_advertisement(format: ObjectFormat, payload: &[u8]) -> Result<RefAdvertisement> {
3473    let payload = trim_trailing_lf(payload);
3474    let (reference, capabilities) = match payload.iter().position(|byte| *byte == 0) {
3475        Some(idx) => (&payload[..idx], parse_capabilities(&payload[idx + 1..])?),
3476        None => (payload, Vec::new()),
3477    };
3478    let text =
3479        std::str::from_utf8(reference).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
3480    let (oid, name) = text
3481        .split_once(' ')
3482        .ok_or_else(|| GitError::InvalidFormat("advertised ref is missing name".into()))?;
3483    if name.is_empty() {
3484        return Err(GitError::InvalidFormat(
3485            "advertised ref name is empty".into(),
3486        ));
3487    }
3488    Ok(RefAdvertisement {
3489        oid: ObjectId::from_hex(format, oid)?,
3490        name: name.to_string(),
3491        capabilities,
3492    })
3493}
3494
3495pub fn encode_ref_advertisement(advertisement: &RefAdvertisement) -> Result<Vec<u8>> {
3496    validate_protocol_v2_token("advertised ref name", &advertisement.name)?;
3497    let mut out = advertisement.oid.to_string().into_bytes();
3498    out.push(b' ');
3499    out.extend_from_slice(advertisement.name.as_bytes());
3500    if !advertisement.capabilities.is_empty() {
3501        out.push(0);
3502        out.extend_from_slice(&encode_capabilities(&advertisement.capabilities)?);
3503    }
3504    out.push(b'\n');
3505    Ok(out)
3506}
3507
3508pub fn parse_ref_advertisements(
3509    format: ObjectFormat,
3510    frames: &[PktLineFrame],
3511) -> Result<Vec<RefAdvertisement>> {
3512    Ok(parse_ref_advertisement_set(format, frames)?.refs)
3513}
3514
3515pub fn parse_ref_advertisement_set(
3516    format: ObjectFormat,
3517    frames: &[PktLineFrame],
3518) -> Result<RefAdvertisementSet> {
3519    let mut set = RefAdvertisementSet {
3520        protocol: ProtocolVersion::V0,
3521        refs: Vec::new(),
3522        shallow: Vec::new(),
3523    };
3524    let mut saw_flush = false;
3525    let mut in_shallow = false;
3526    for (idx, frame) in frames.iter().enumerate() {
3527        match frame {
3528            PktLineFrame::Data(payload) if !saw_flush => {
3529                let trimmed = trim_trailing_lf(payload);
3530                if trimmed == b"version 1" {
3531                    if idx != 0 {
3532                        return Err(GitError::InvalidFormat(
3533                            "advertised ref protocol version must be the first line".into(),
3534                        ));
3535                    }
3536                    set.protocol = ProtocolVersion::V1;
3537                    continue;
3538                }
3539                if trimmed.starts_with(b"version ") {
3540                    return Err(GitError::InvalidFormat(
3541                        "unsupported advertised ref protocol version".into(),
3542                    ));
3543                }
3544                if trimmed.starts_with(b"shallow ") {
3545                    if set.refs.is_empty() {
3546                        return Err(GitError::InvalidFormat(
3547                            "advertised shallow refs must follow advertised refs".into(),
3548                        ));
3549                    }
3550                    let text = parse_protocol_v2_line_text("advertised shallow ref", payload)?;
3551                    set.shallow.push(parse_oid_argument(
3552                        format,
3553                        "advertised shallow ref",
3554                        text,
3555                        "shallow ",
3556                    )?);
3557                    in_shallow = true;
3558                    continue;
3559                }
3560                if in_shallow {
3561                    return Err(GitError::InvalidFormat(
3562                        "advertised refs must not follow shallow refs".into(),
3563                    ));
3564                }
3565                let advertisement = parse_ref_advertisement(format, payload)?;
3566                if !set.refs.is_empty() && !advertisement.capabilities.is_empty() {
3567                    return Err(GitError::InvalidFormat(
3568                        "advertised ref capabilities must appear on the first ref".into(),
3569                    ));
3570                }
3571                set.refs.push(advertisement);
3572            }
3573            PktLineFrame::Data(_) => {
3574                return Err(GitError::InvalidFormat(
3575                    "advertised ref stream has data after flush".into(),
3576                ));
3577            }
3578            PktLineFrame::Flush => {
3579                saw_flush = true;
3580                if idx + 1 != frames.len() {
3581                    return Err(GitError::InvalidFormat(
3582                        "advertised ref stream has frames after flush".into(),
3583                    ));
3584                }
3585            }
3586            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
3587                return Err(GitError::InvalidFormat(
3588                    "advertised ref stream contains a non-flush control packet".into(),
3589                ));
3590            }
3591        }
3592    }
3593    if !saw_flush {
3594        return Err(GitError::InvalidFormat(
3595            "advertised ref stream missing flush".into(),
3596        ));
3597    }
3598    Ok(set)
3599}
3600
3601pub fn encode_ref_advertisements(advertisements: &[RefAdvertisement]) -> Result<Vec<PktLineFrame>> {
3602    encode_ref_advertisement_set(&RefAdvertisementSet {
3603        protocol: ProtocolVersion::V0,
3604        refs: advertisements.to_vec(),
3605        shallow: Vec::new(),
3606    })
3607}
3608
3609pub fn encode_ref_advertisement_set(set: &RefAdvertisementSet) -> Result<Vec<PktLineFrame>> {
3610    let mut frames = Vec::new();
3611    match set.protocol {
3612        ProtocolVersion::V0 => {}
3613        ProtocolVersion::V1 => frames.push(PktLineFrame::data(line_from_str("version 1"))?),
3614        ProtocolVersion::V2 => {
3615            return Err(GitError::InvalidFormat(
3616                "protocol v2 does not use v0/v1 advertised-ref streams".into(),
3617            ));
3618        }
3619    }
3620    if set.refs.is_empty() && !set.shallow.is_empty() {
3621        return Err(GitError::InvalidFormat(
3622            "advertised shallow refs require advertised refs".into(),
3623        ));
3624    }
3625    for (idx, advertisement) in set.refs.iter().enumerate() {
3626        if idx != 0 && !advertisement.capabilities.is_empty() {
3627            return Err(GitError::InvalidFormat(
3628                "advertised ref capabilities must appear on the first ref".into(),
3629            ));
3630        }
3631        frames.push(PktLineFrame::data(encode_ref_advertisement(
3632            advertisement,
3633        )?)?);
3634    }
3635    for oid in &set.shallow {
3636        frames.push(PktLineFrame::data(line_from_str(&format!(
3637            "shallow {oid}"
3638        )))?);
3639    }
3640    frames.push(PktLineFrame::Flush);
3641    Ok(frames)
3642}
3643
3644pub fn read_ref_advertisements(
3645    format: ObjectFormat,
3646    reader: &mut impl Read,
3647) -> Result<Vec<RefAdvertisement>> {
3648    let frames = read_pkt_line_frames_until_flush(reader)?;
3649    parse_ref_advertisements(format, &frames)
3650}
3651
3652pub fn read_ref_advertisement_set(
3653    format: ObjectFormat,
3654    reader: &mut impl Read,
3655) -> Result<RefAdvertisementSet> {
3656    let frames = read_pkt_line_frames_until_flush(reader)?;
3657    parse_ref_advertisement_set(format, &frames)
3658}
3659
3660pub fn write_ref_advertisements(
3661    writer: &mut impl Write,
3662    advertisements: &[RefAdvertisement],
3663) -> Result<()> {
3664    write_ref_advertisement_stream(writer, ProtocolVersion::V0, advertisements, &[])
3665}
3666
3667pub fn write_ref_advertisement_set(
3668    writer: &mut impl Write,
3669    set: &RefAdvertisementSet,
3670) -> Result<()> {
3671    write_ref_advertisement_stream(writer, set.protocol, &set.refs, &set.shallow)
3672}
3673
3674fn write_ref_advertisement_stream(
3675    writer: &mut impl Write,
3676    protocol: ProtocolVersion,
3677    refs: &[RefAdvertisement],
3678    shallow: &[ObjectId],
3679) -> Result<()> {
3680    match protocol {
3681        ProtocolVersion::V0 => {}
3682        ProtocolVersion::V1 => write_pkt_line_payload(writer, b"version 1\n")?,
3683        ProtocolVersion::V2 => {
3684            return Err(GitError::InvalidFormat(
3685                "protocol v2 does not use v0/v1 advertised-ref streams".into(),
3686            ));
3687        }
3688    }
3689    if refs.is_empty() && !shallow.is_empty() {
3690        return Err(GitError::InvalidFormat(
3691            "advertised shallow refs require advertised refs".into(),
3692        ));
3693    }
3694    for (idx, advertisement) in refs.iter().enumerate() {
3695        if idx != 0 && !advertisement.capabilities.is_empty() {
3696            return Err(GitError::InvalidFormat(
3697                "advertised ref capabilities must appear on the first ref".into(),
3698            ));
3699        }
3700        write_pkt_line_payload(writer, &encode_ref_advertisement(advertisement)?)?;
3701    }
3702    for oid in shallow {
3703        write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
3704    }
3705    writer.write_all(b"0000")?;
3706    Ok(())
3707}
3708
3709pub fn parse_dumb_http_info_refs(
3710    format: ObjectFormat,
3711    input: &[u8],
3712) -> Result<Vec<DumbHttpRefRecord>> {
3713    if input.is_empty() {
3714        return Ok(Vec::new());
3715    }
3716    input
3717        .split_inclusive(|byte| *byte == b'\n')
3718        .map(|line| parse_dumb_http_info_ref_record(format, line))
3719        .collect()
3720}
3721
3722pub fn encode_dumb_http_info_refs(records: &[DumbHttpRefRecord]) -> Result<Vec<u8>> {
3723    let mut out = Vec::new();
3724    for record in records {
3725        validate_dumb_http_ref_name(&record.name)?;
3726        out.extend_from_slice(record.oid.to_string().as_bytes());
3727        out.push(b'\t');
3728        out.extend_from_slice(record.name.as_bytes());
3729        if record.peeled {
3730            out.extend_from_slice(b"^{}");
3731        }
3732        out.push(b'\n');
3733    }
3734    Ok(out)
3735}
3736
3737pub fn read_dumb_http_info_refs(
3738    format: ObjectFormat,
3739    reader: &mut impl Read,
3740) -> Result<Vec<DumbHttpRefRecord>> {
3741    let mut input = Vec::new();
3742    reader.read_to_end(&mut input)?;
3743    parse_dumb_http_info_refs(format, &input)
3744}
3745
3746pub fn write_dumb_http_info_refs(
3747    writer: &mut impl Write,
3748    records: &[DumbHttpRefRecord],
3749) -> Result<()> {
3750    for record in records {
3751        validate_dumb_http_ref_name(&record.name)?;
3752        writer.write_all(record.oid.to_string().as_bytes())?;
3753        writer.write_all(b"\t")?;
3754        writer.write_all(record.name.as_bytes())?;
3755        if record.peeled {
3756            writer.write_all(b"^{}")?;
3757        }
3758        writer.write_all(b"\n")?;
3759    }
3760    Ok(())
3761}
3762
3763pub fn parse_dumb_http_alternates(input: &[u8]) -> Result<Vec<String>> {
3764    if input.is_empty() {
3765        return Ok(Vec::new());
3766    }
3767    input
3768        .split_inclusive(|byte| *byte == b'\n')
3769        .map(parse_dumb_http_alternate)
3770        .collect()
3771}
3772
3773pub fn encode_dumb_http_alternates(alternates: &[String]) -> Result<Vec<u8>> {
3774    let mut out = Vec::new();
3775    for alternate in alternates {
3776        validate_dumb_http_alternate(alternate)?;
3777        out.extend_from_slice(alternate.as_bytes());
3778        out.push(b'\n');
3779    }
3780    Ok(out)
3781}
3782
3783pub fn read_dumb_http_alternates(reader: &mut impl Read) -> Result<Vec<String>> {
3784    let mut input = Vec::new();
3785    reader.read_to_end(&mut input)?;
3786    parse_dumb_http_alternates(&input)
3787}
3788
3789pub fn write_dumb_http_alternates(writer: &mut impl Write, alternates: &[String]) -> Result<()> {
3790    for alternate in alternates {
3791        validate_dumb_http_alternate(alternate)?;
3792        writer.write_all(alternate.as_bytes())?;
3793        writer.write_all(b"\n")?;
3794    }
3795    Ok(())
3796}
3797
3798pub fn parse_dumb_http_packs(
3799    format: ObjectFormat,
3800    input: &[u8],
3801) -> Result<Vec<DumbHttpPackRecord>> {
3802    if input.is_empty() {
3803        return Ok(Vec::new());
3804    }
3805    input
3806        .split_inclusive(|byte| *byte == b'\n')
3807        .map(|line| parse_dumb_http_pack_record(format, line))
3808        .collect()
3809}
3810
3811pub fn encode_dumb_http_packs(records: &[DumbHttpPackRecord]) -> Result<Vec<u8>> {
3812    let mut out = Vec::new();
3813    for record in records {
3814        out.extend_from_slice(format!("P pack-{}.pack\n", record.hash).as_bytes());
3815    }
3816    Ok(out)
3817}
3818
3819pub fn read_dumb_http_packs(
3820    format: ObjectFormat,
3821    reader: &mut impl Read,
3822) -> Result<Vec<DumbHttpPackRecord>> {
3823    let mut input = Vec::new();
3824    reader.read_to_end(&mut input)?;
3825    parse_dumb_http_packs(format, &input)
3826}
3827
3828pub fn write_dumb_http_packs(
3829    writer: &mut impl Write,
3830    records: &[DumbHttpPackRecord],
3831) -> Result<()> {
3832    for record in records {
3833        writer.write_all(format!("P pack-{}.pack\n", record.hash).as_bytes())?;
3834    }
3835    Ok(())
3836}
3837
3838pub fn parse_upload_pack_request(
3839    format: ObjectFormat,
3840    frames: &[PktLineFrame],
3841) -> Result<Option<UploadPackRequest>> {
3842    if matches!(frames, [PktLineFrame::Flush]) {
3843        return Ok(None);
3844    }
3845
3846    let mut request = UploadPackRequest::default();
3847    let mut in_options = false;
3848    let mut saw_flush = false;
3849    for (idx, frame) in frames.iter().enumerate() {
3850        match frame {
3851            PktLineFrame::Data(payload) if !saw_flush => {
3852                let text = parse_protocol_v2_line_text("upload-pack request line", payload)?;
3853                if let Some(value) = text.strip_prefix("want ") {
3854                    if in_options {
3855                        return Err(GitError::InvalidFormat(
3856                            "upload-pack request has want after options".into(),
3857                        ));
3858                    }
3859                    let (oid, capabilities) = if request.wants.is_empty() {
3860                        value
3861                            .split_once(' ')
3862                            .map_or((value, None), |(oid, caps)| (oid, Some(caps.as_bytes())))
3863                    } else {
3864                        if value.contains(' ') {
3865                            return Err(GitError::InvalidFormat(
3866                                "additional upload-pack want has capabilities".into(),
3867                            ));
3868                        }
3869                        (value, None)
3870                    };
3871                    validate_protocol_v2_token("upload-pack want", oid)?;
3872                    request.wants.push(ObjectId::from_hex(format, oid)?);
3873                    if let Some(capabilities) = capabilities {
3874                        request.capabilities = parse_capabilities(capabilities)?;
3875                    }
3876                    continue;
3877                }
3878
3879                if request.wants.is_empty() {
3880                    return Err(GitError::InvalidFormat(
3881                        "upload-pack request must start with want".into(),
3882                    ));
3883                }
3884                in_options = true;
3885                if text.starts_with("shallow ") {
3886                    request.shallow.push(parse_oid_argument(
3887                        format,
3888                        "upload-pack shallow",
3889                        text,
3890                        "shallow ",
3891                    )?);
3892                } else if text.starts_with("deepen ") {
3893                    if request.deepen.is_some() {
3894                        return Err(GitError::InvalidFormat(
3895                            "upload-pack request has duplicate deepen".into(),
3896                        ));
3897                    }
3898                    request.deepen =
3899                        Some(parse_u32_argument("upload-pack deepen", text, "deepen ")?);
3900                } else if text.starts_with("deepen-since ") {
3901                    if request.deepen_since.is_some() {
3902                        return Err(GitError::InvalidFormat(
3903                            "upload-pack request has duplicate deepen-since".into(),
3904                        ));
3905                    }
3906                    request.deepen_since = Some(parse_u64_argument(
3907                        "upload-pack deepen-since",
3908                        text,
3909                        "deepen-since ",
3910                    )?);
3911                } else if let Some(name) = text.strip_prefix("deepen-not ") {
3912                    validate_protocol_v2_token("upload-pack deepen-not", name)?;
3913                    request.deepen_not.push(name.to_string());
3914                } else if let Some(filter) = text.strip_prefix("filter ") {
3915                    if request.filter.is_some() {
3916                        return Err(GitError::InvalidFormat(
3917                            "upload-pack request has duplicate filter".into(),
3918                        ));
3919                    }
3920                    validate_protocol_v2_token("upload-pack filter", filter)?;
3921                    request.filter = Some(filter.to_string());
3922                } else {
3923                    return Err(GitError::InvalidFormat(format!(
3924                        "unsupported upload-pack request line {text}"
3925                    )));
3926                }
3927            }
3928            PktLineFrame::Data(_) => {
3929                return Err(GitError::InvalidFormat(
3930                    "upload-pack request has data after flush".into(),
3931                ));
3932            }
3933            PktLineFrame::Flush => {
3934                saw_flush = true;
3935                if idx + 1 != frames.len() {
3936                    return Err(GitError::InvalidFormat(
3937                        "upload-pack request has frames after flush".into(),
3938                    ));
3939                }
3940            }
3941            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
3942                return Err(GitError::InvalidFormat(
3943                    "upload-pack request contains a non-flush control packet".into(),
3944                ));
3945            }
3946        }
3947    }
3948    if !saw_flush {
3949        return Err(GitError::InvalidFormat(
3950            "upload-pack request missing flush".into(),
3951        ));
3952    }
3953    if request.wants.is_empty() {
3954        return Err(GitError::InvalidFormat(
3955            "upload-pack request missing want".into(),
3956        ));
3957    }
3958    Ok(Some(request))
3959}
3960
3961pub fn encode_upload_pack_request(
3962    request: Option<&UploadPackRequest>,
3963) -> Result<Vec<PktLineFrame>> {
3964    let Some(request) = request else {
3965        return Ok(vec![PktLineFrame::Flush]);
3966    };
3967    if request.wants.is_empty() {
3968        return Err(GitError::InvalidFormat(
3969            "upload-pack request missing want".into(),
3970        ));
3971    }
3972
3973    let mut frames = Vec::new();
3974    for (idx, oid) in request.wants.iter().enumerate() {
3975        let mut line = format!("want {oid}");
3976        if idx == 0 && !request.capabilities.is_empty() {
3977            line.push(' ');
3978            line.push_str(
3979                &String::from_utf8(encode_capabilities(&request.capabilities)?)
3980                    .map_err(|err| GitError::InvalidFormat(err.to_string()))?,
3981            );
3982        }
3983        frames.push(PktLineFrame::data(line_from_str(&line))?);
3984    }
3985    for oid in &request.shallow {
3986        frames.push(PktLineFrame::data(line_from_str(&format!(
3987            "shallow {oid}"
3988        )))?);
3989    }
3990    if let Some(deepen) = request.deepen {
3991        if deepen == 0 {
3992            return Err(GitError::InvalidFormat(
3993                "upload-pack deepen must be positive".into(),
3994            ));
3995        }
3996        frames.push(PktLineFrame::data(line_from_str(&format!(
3997            "deepen {deepen}"
3998        )))?);
3999    }
4000    if let Some(deepen_since) = request.deepen_since {
4001        frames.push(PktLineFrame::data(line_from_str(&format!(
4002            "deepen-since {deepen_since}"
4003        )))?);
4004    }
4005    for name in &request.deepen_not {
4006        validate_protocol_v2_token("upload-pack deepen-not", name)?;
4007        frames.push(PktLineFrame::data(line_from_str(&format!(
4008            "deepen-not {name}"
4009        )))?);
4010    }
4011    if let Some(filter) = &request.filter {
4012        validate_protocol_v2_token("upload-pack filter", filter)?;
4013        frames.push(PktLineFrame::data(line_from_str(&format!(
4014            "filter {filter}"
4015        )))?);
4016    }
4017    frames.push(PktLineFrame::Flush);
4018    Ok(frames)
4019}
4020
4021pub fn read_upload_pack_request(
4022    format: ObjectFormat,
4023    reader: &mut impl Read,
4024) -> Result<Option<UploadPackRequest>> {
4025    let frames = read_pkt_line_frames_until_flush(reader)?;
4026    parse_upload_pack_request(format, &frames)
4027}
4028
4029pub fn write_upload_pack_request(
4030    writer: &mut impl Write,
4031    request: Option<&UploadPackRequest>,
4032) -> Result<()> {
4033    let Some(request) = request else {
4034        writer.write_all(b"0000")?;
4035        return Ok(());
4036    };
4037    if request.wants.is_empty() {
4038        return Err(GitError::InvalidFormat(
4039            "upload-pack request missing want".into(),
4040        ));
4041    }
4042
4043    for (idx, oid) in request.wants.iter().enumerate() {
4044        let mut line = format!("want {oid}");
4045        if idx == 0 && !request.capabilities.is_empty() {
4046            line.push(' ');
4047            line.push_str(
4048                &String::from_utf8(encode_capabilities(&request.capabilities)?)
4049                    .map_err(|err| GitError::InvalidFormat(err.to_string()))?,
4050            );
4051        }
4052        write_pkt_line_payload(writer, &line_from_str(&line))?;
4053    }
4054    for oid in &request.shallow {
4055        write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
4056    }
4057    if let Some(deepen) = request.deepen {
4058        if deepen == 0 {
4059            return Err(GitError::InvalidFormat(
4060                "upload-pack deepen must be positive".into(),
4061            ));
4062        }
4063        write_pkt_line_payload(writer, &line_from_str(&format!("deepen {deepen}")))?;
4064    }
4065    if let Some(deepen_since) = request.deepen_since {
4066        write_pkt_line_payload(
4067            writer,
4068            &line_from_str(&format!("deepen-since {deepen_since}")),
4069        )?;
4070    }
4071    for name in &request.deepen_not {
4072        validate_protocol_v2_token("upload-pack deepen-not", name)?;
4073        write_pkt_line_payload(writer, &line_from_str(&format!("deepen-not {name}")))?;
4074    }
4075    if let Some(filter) = &request.filter {
4076        validate_protocol_v2_token("upload-pack filter", filter)?;
4077        write_pkt_line_payload(writer, &line_from_str(&format!("filter {filter}")))?;
4078    }
4079    writer.write_all(b"0000")?;
4080    Ok(())
4081}
4082
4083pub fn parse_upload_pack_features(capabilities: &[Capability]) -> Result<UploadPackFeatures> {
4084    let mut features = UploadPackFeatures::default();
4085    for capability in capabilities {
4086        match capability.name.as_str() {
4087            "multi_ack" => set_upload_pack_flag(&mut features.multi_ack, capability)?,
4088            "multi_ack_detailed" => {
4089                set_upload_pack_flag(&mut features.multi_ack_detailed, capability)?
4090            }
4091            "no-done" => set_upload_pack_flag(&mut features.no_done, capability)?,
4092            "thin-pack" => set_upload_pack_flag(&mut features.thin_pack, capability)?,
4093            "side-band" => set_upload_pack_flag(&mut features.side_band, capability)?,
4094            "side-band-64k" => set_upload_pack_flag(&mut features.side_band_64k, capability)?,
4095            "ofs-delta" => set_upload_pack_flag(&mut features.ofs_delta, capability)?,
4096            "shallow" => set_upload_pack_flag(&mut features.shallow, capability)?,
4097            "deepen-since" => set_upload_pack_flag(&mut features.deepen_since, capability)?,
4098            "deepen-not" => set_upload_pack_flag(&mut features.deepen_not, capability)?,
4099            "include-tag" => set_upload_pack_flag(&mut features.include_tag, capability)?,
4100            "no-progress" => set_upload_pack_flag(&mut features.no_progress, capability)?,
4101            "allow-tip-sha1-in-want" => {
4102                set_upload_pack_flag(&mut features.allow_tip_sha1_in_want, capability)?
4103            }
4104            "allow-reachable-sha1-in-want" => {
4105                set_upload_pack_flag(&mut features.allow_reachable_sha1_in_want, capability)?
4106            }
4107            "filter" => set_upload_pack_flag(&mut features.filter, capability)?,
4108            "agent" => {
4109                let Some(agent) = &capability.value else {
4110                    return Err(GitError::InvalidFormat(
4111                        "upload-pack agent capability is missing value".into(),
4112                    ));
4113                };
4114                if features.agent.is_some() {
4115                    return Err(GitError::InvalidFormat(
4116                        "upload-pack has duplicate agent capability".into(),
4117                    ));
4118                }
4119                validate_capability_field("upload-pack agent", agent)?;
4120                features.agent = Some(agent.clone());
4121            }
4122            "object-format" => {
4123                let Some(format) = &capability.value else {
4124                    return Err(GitError::InvalidFormat(
4125                        "upload-pack object-format capability is missing value".into(),
4126                    ));
4127                };
4128                if features.object_format.is_some() {
4129                    return Err(GitError::InvalidFormat(
4130                        "upload-pack has duplicate object-format capability".into(),
4131                    ));
4132                }
4133                validate_capability_field("upload-pack object-format", format)?;
4134                features.object_format = Some(format.parse()?);
4135            }
4136            "symref" => {
4137                let Some(symref) = &capability.value else {
4138                    return Err(GitError::InvalidFormat(
4139                        "upload-pack symref capability is missing value".into(),
4140                    ));
4141                };
4142                validate_capability_field("upload-pack symref", symref)?;
4143                features.symrefs.push(symref.clone());
4144            }
4145            _ => {
4146                encode_capabilities(std::slice::from_ref(capability))?;
4147                if features
4148                    .unknown
4149                    .iter()
4150                    .any(|known| known.name == capability.name)
4151                {
4152                    return Err(GitError::InvalidFormat(format!(
4153                        "upload-pack has duplicate {} capability",
4154                        capability.name
4155                    )));
4156                }
4157                features.unknown.push(capability.clone());
4158            }
4159        }
4160    }
4161    Ok(features)
4162}
4163
4164pub fn encode_upload_pack_features(features: &UploadPackFeatures) -> Result<Vec<Capability>> {
4165    let mut capabilities = Vec::new();
4166    push_upload_pack_flag(&mut capabilities, "multi_ack", features.multi_ack);
4167    push_upload_pack_flag(
4168        &mut capabilities,
4169        "multi_ack_detailed",
4170        features.multi_ack_detailed,
4171    );
4172    push_upload_pack_flag(&mut capabilities, "no-done", features.no_done);
4173    push_upload_pack_flag(&mut capabilities, "thin-pack", features.thin_pack);
4174    push_upload_pack_flag(&mut capabilities, "side-band", features.side_band);
4175    push_upload_pack_flag(&mut capabilities, "side-band-64k", features.side_band_64k);
4176    push_upload_pack_flag(&mut capabilities, "ofs-delta", features.ofs_delta);
4177    push_upload_pack_flag(&mut capabilities, "shallow", features.shallow);
4178    push_upload_pack_flag(&mut capabilities, "deepen-since", features.deepen_since);
4179    push_upload_pack_flag(&mut capabilities, "deepen-not", features.deepen_not);
4180    push_upload_pack_flag(&mut capabilities, "include-tag", features.include_tag);
4181    push_upload_pack_flag(&mut capabilities, "no-progress", features.no_progress);
4182    push_upload_pack_flag(
4183        &mut capabilities,
4184        "allow-tip-sha1-in-want",
4185        features.allow_tip_sha1_in_want,
4186    );
4187    push_upload_pack_flag(
4188        &mut capabilities,
4189        "allow-reachable-sha1-in-want",
4190        features.allow_reachable_sha1_in_want,
4191    );
4192    push_upload_pack_flag(&mut capabilities, "filter", features.filter);
4193    if let Some(agent) = &features.agent {
4194        validate_capability_field("upload-pack agent", agent)?;
4195        capabilities.push(Capability {
4196            name: "agent".into(),
4197            value: Some(agent.clone()),
4198        });
4199    }
4200    if let Some(format) = features.object_format {
4201        capabilities.push(Capability {
4202            name: "object-format".into(),
4203            value: Some(format.name().into()),
4204        });
4205    }
4206    for symref in &features.symrefs {
4207        validate_capability_field("upload-pack symref", symref)?;
4208        capabilities.push(Capability {
4209            name: "symref".into(),
4210            value: Some(symref.clone()),
4211        });
4212    }
4213    for capability in &features.unknown {
4214        if is_known_upload_pack_capability(&capability.name) {
4215            return Err(GitError::InvalidFormat(format!(
4216                "upload-pack unknown capability duplicates known capability {}",
4217                capability.name
4218            )));
4219        }
4220        encode_capabilities(std::slice::from_ref(capability))?;
4221        capabilities.push(capability.clone());
4222    }
4223    Ok(capabilities)
4224}
4225
4226pub fn validate_upload_pack_request_features(
4227    features: &UploadPackFeatures,
4228    request: &UploadPackRequest,
4229) -> Result<()> {
4230    for capability in &request.capabilities {
4231        if is_upload_pack_flag_capability(&capability.name) {
4232            reject_capability_value("upload-pack request capability", capability)?;
4233        }
4234        match capability.name.as_str() {
4235            "multi_ack" if !features.multi_ack => {
4236                return Err(GitError::InvalidFormat(
4237                    "upload-pack request uses multi_ack without advertised capability".into(),
4238                ));
4239            }
4240            "multi_ack_detailed" if !features.multi_ack_detailed => {
4241                return Err(GitError::InvalidFormat(
4242                    "upload-pack request uses multi_ack_detailed without advertised capability"
4243                        .into(),
4244                ));
4245            }
4246            "no-done" if !features.no_done => {
4247                return Err(GitError::InvalidFormat(
4248                    "upload-pack request uses no-done without advertised capability".into(),
4249                ));
4250            }
4251            "thin-pack" if !features.thin_pack => {
4252                return Err(GitError::InvalidFormat(
4253                    "upload-pack request uses thin-pack without advertised capability".into(),
4254                ));
4255            }
4256            "side-band" if !features.side_band => {
4257                return Err(GitError::InvalidFormat(
4258                    "upload-pack request uses side-band without advertised capability".into(),
4259                ));
4260            }
4261            "side-band-64k" if !features.side_band_64k => {
4262                return Err(GitError::InvalidFormat(
4263                    "upload-pack request uses side-band-64k without advertised capability".into(),
4264                ));
4265            }
4266            "ofs-delta" if !features.ofs_delta => {
4267                return Err(GitError::InvalidFormat(
4268                    "upload-pack request uses ofs-delta without advertised capability".into(),
4269                ));
4270            }
4271            "include-tag" if !features.include_tag => {
4272                return Err(GitError::InvalidFormat(
4273                    "upload-pack request uses include-tag without advertised capability".into(),
4274                ));
4275            }
4276            "no-progress" if !features.no_progress => {
4277                return Err(GitError::InvalidFormat(
4278                    "upload-pack request uses no-progress without advertised capability".into(),
4279                ));
4280            }
4281            "allow-tip-sha1-in-want" if !features.allow_tip_sha1_in_want => {
4282                return Err(GitError::InvalidFormat(
4283                    "upload-pack request uses allow-tip-sha1-in-want without advertised capability"
4284                        .into(),
4285                ));
4286            }
4287            "allow-reachable-sha1-in-want" if !features.allow_reachable_sha1_in_want => {
4288                return Err(GitError::InvalidFormat(
4289                    "upload-pack request uses allow-reachable-sha1-in-want without advertised capability"
4290                        .into(),
4291                ));
4292            }
4293            "filter" if !features.filter => {
4294                return Err(GitError::InvalidFormat(
4295                    "upload-pack request uses filter capability without advertised capability"
4296                        .into(),
4297                ));
4298            }
4299            "agent" => {
4300                let Some(agent) = &capability.value else {
4301                    return Err(GitError::InvalidFormat(
4302                        "upload-pack request agent capability is missing value".into(),
4303                    ));
4304                };
4305                validate_capability_field("upload-pack request agent", agent)?;
4306            }
4307            "object-format" => {
4308                let Some(format) = &capability.value else {
4309                    return Err(GitError::InvalidFormat(
4310                        "upload-pack request object-format capability is missing value".into(),
4311                    ));
4312                };
4313                let requested_format: ObjectFormat = format.parse()?;
4314                if features.object_format != Some(requested_format) {
4315                    return Err(GitError::InvalidFormat(
4316                        "upload-pack request object-format was not advertised".into(),
4317                    ));
4318                }
4319            }
4320            name if is_known_upload_pack_capability(name) => {}
4321            _ => {
4322                if !features
4323                    .unknown
4324                    .iter()
4325                    .any(|advertised| advertised.name == capability.name)
4326                {
4327                    return Err(GitError::InvalidFormat(format!(
4328                        "upload-pack request uses unadvertised capability {}",
4329                        capability.name
4330                    )));
4331                }
4332            }
4333        }
4334    }
4335
4336    let sideband = request
4337        .capabilities
4338        .iter()
4339        .any(|capability| capability.name == "side-band");
4340    let sideband_64k = request
4341        .capabilities
4342        .iter()
4343        .any(|capability| capability.name == "side-band-64k");
4344    if sideband && sideband_64k {
4345        return Err(GitError::InvalidFormat(
4346            "upload-pack request must not request both side-band and side-band-64k".into(),
4347        ));
4348    }
4349
4350    if !features.shallow && (!request.shallow.is_empty() || request.deepen.is_some()) {
4351        return Err(GitError::InvalidFormat(
4352            "upload-pack request uses shallow/deepen without advertised shallow capability".into(),
4353        ));
4354    }
4355    if !features.deepen_since && request.deepen_since.is_some() {
4356        return Err(GitError::InvalidFormat(
4357            "upload-pack request uses deepen-since without advertised capability".into(),
4358        ));
4359    }
4360    if !features.deepen_not && !request.deepen_not.is_empty() {
4361        return Err(GitError::InvalidFormat(
4362            "upload-pack request uses deepen-not without advertised capability".into(),
4363        ));
4364    }
4365    if !features.filter && request.filter.is_some() {
4366        return Err(GitError::InvalidFormat(
4367            "upload-pack request uses filter without advertised capability".into(),
4368        ));
4369    }
4370    Ok(())
4371}
4372
4373pub fn build_upload_pack_raw_packfile_response<C, B>(
4374    features: &UploadPackFeatures,
4375    request: UploadPackRequest,
4376    haves: impl IntoIterator<Item = ObjectId>,
4377    mut contains_object: C,
4378    mut build_pack: B,
4379) -> Result<UploadPackRawPackfileResponse>
4380where
4381    C: FnMut(&ObjectId) -> Result<bool>,
4382    B: FnMut(Vec<ObjectId>, Vec<ObjectId>) -> Result<Option<Vec<u8>>>,
4383{
4384    validate_upload_pack_request_features(features, &request)?;
4385    for want in &request.wants {
4386        if !contains_object(want)? {
4387            return Err(GitError::InvalidObject(format!(
4388                "upload-pack requested missing object {want}"
4389            )));
4390        }
4391    }
4392    let known_haves = haves
4393        .into_iter()
4394        .filter_map(|oid| match contains_object(&oid) {
4395            Ok(true) => Some(Ok(oid)),
4396            Ok(false) => None,
4397            Err(err) => Some(Err(err)),
4398        })
4399        .collect::<Result<Vec<_>>>()?;
4400    let packfile = build_pack(request.wants, known_haves)?
4401        .ok_or_else(|| GitError::InvalidObject("upload-pack request produced empty pack".into()))?;
4402    Ok(UploadPackRawPackfileResponse {
4403        acknowledgments: vec![UploadPackAcknowledgment::Nak],
4404        packfile,
4405    })
4406}
4407
4408pub fn parse_upload_pack_shallow_update(
4409    format: ObjectFormat,
4410    frames: &[PktLineFrame],
4411) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
4412    let mut entries = Vec::new();
4413    let mut saw_flush = false;
4414    for (idx, frame) in frames.iter().enumerate() {
4415        match frame {
4416            PktLineFrame::Data(payload) if !saw_flush => {
4417                entries.push(parse_fetch_shallow_info(format, payload)?);
4418            }
4419            PktLineFrame::Data(_) => {
4420                return Err(GitError::InvalidFormat(
4421                    "upload-pack shallow update has data after flush".into(),
4422                ));
4423            }
4424            PktLineFrame::Flush => {
4425                saw_flush = true;
4426                if idx + 1 != frames.len() {
4427                    return Err(GitError::InvalidFormat(
4428                        "upload-pack shallow update has frames after flush".into(),
4429                    ));
4430                }
4431            }
4432            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4433                return Err(GitError::InvalidFormat(
4434                    "upload-pack shallow update contains a non-flush control packet".into(),
4435                ));
4436            }
4437        }
4438    }
4439    if !saw_flush {
4440        return Err(GitError::InvalidFormat(
4441            "upload-pack shallow update missing flush".into(),
4442        ));
4443    }
4444    Ok(entries)
4445}
4446
4447pub fn encode_upload_pack_shallow_update(
4448    entries: &[ProtocolV2FetchShallowInfo],
4449) -> Result<Vec<PktLineFrame>> {
4450    let mut frames = Vec::new();
4451    for entry in entries {
4452        let line = match entry {
4453            ProtocolV2FetchShallowInfo::Shallow(oid) => format!("shallow {oid}"),
4454            ProtocolV2FetchShallowInfo::Unshallow(oid) => format!("unshallow {oid}"),
4455        };
4456        frames.push(PktLineFrame::data(line_from_str(&line))?);
4457    }
4458    frames.push(PktLineFrame::Flush);
4459    Ok(frames)
4460}
4461
4462pub fn read_upload_pack_shallow_update(
4463    format: ObjectFormat,
4464    reader: &mut impl Read,
4465) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
4466    let frames = read_pkt_line_frames_until_flush(reader)?;
4467    parse_upload_pack_shallow_update(format, &frames)
4468}
4469
4470pub fn write_upload_pack_shallow_update(
4471    writer: &mut impl Write,
4472    entries: &[ProtocolV2FetchShallowInfo],
4473) -> Result<()> {
4474    for entry in entries {
4475        let line = match entry {
4476            ProtocolV2FetchShallowInfo::Shallow(oid) => format!("shallow {oid}"),
4477            ProtocolV2FetchShallowInfo::Unshallow(oid) => format!("unshallow {oid}"),
4478        };
4479        write_pkt_line_payload(writer, &line_from_str(&line))?;
4480    }
4481    writer.write_all(b"0000")?;
4482    Ok(())
4483}
4484
4485pub fn parse_upload_pack_negotiation_request(
4486    format: ObjectFormat,
4487    frames: &[PktLineFrame],
4488) -> Result<UploadPackNegotiationRequest> {
4489    let mut request = UploadPackNegotiationRequest::default();
4490    let mut terminated = false;
4491    for (idx, frame) in frames.iter().enumerate() {
4492        match frame {
4493            PktLineFrame::Data(payload) if !terminated => {
4494                let text = parse_protocol_v2_line_text("upload-pack negotiation line", payload)?;
4495                if text == "done" {
4496                    request.done = true;
4497                    terminated = true;
4498                    if idx + 1 != frames.len() {
4499                        return Err(GitError::InvalidFormat(
4500                            "upload-pack negotiation has frames after done".into(),
4501                        ));
4502                    }
4503                } else if text.starts_with("have ") {
4504                    request.haves.push(parse_oid_argument(
4505                        format,
4506                        "upload-pack have",
4507                        text,
4508                        "have ",
4509                    )?);
4510                } else {
4511                    return Err(GitError::InvalidFormat(format!(
4512                        "unsupported upload-pack negotiation line {text}"
4513                    )));
4514                }
4515            }
4516            PktLineFrame::Data(_) => {
4517                return Err(GitError::InvalidFormat(
4518                    "upload-pack negotiation has data after terminator".into(),
4519                ));
4520            }
4521            PktLineFrame::Flush => {
4522                terminated = true;
4523                if idx + 1 != frames.len() {
4524                    return Err(GitError::InvalidFormat(
4525                        "upload-pack negotiation has frames after flush".into(),
4526                    ));
4527                }
4528            }
4529            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4530                return Err(GitError::InvalidFormat(
4531                    "upload-pack negotiation contains a non-flush control packet".into(),
4532                ));
4533            }
4534        }
4535    }
4536    if !terminated {
4537        return Err(GitError::InvalidFormat(
4538            "upload-pack negotiation missing terminator".into(),
4539        ));
4540    }
4541    Ok(request)
4542}
4543
4544pub fn encode_upload_pack_negotiation_request(
4545    request: &UploadPackNegotiationRequest,
4546) -> Result<Vec<PktLineFrame>> {
4547    let mut frames = Vec::new();
4548    for oid in &request.haves {
4549        frames.push(PktLineFrame::data(line_from_str(&format!("have {oid}")))?);
4550    }
4551    if request.done {
4552        frames.push(PktLineFrame::data(line_from_str("done"))?);
4553    } else {
4554        frames.push(PktLineFrame::Flush);
4555    }
4556    Ok(frames)
4557}
4558
4559pub fn read_upload_pack_negotiation_request(
4560    format: ObjectFormat,
4561    reader: &mut impl Read,
4562) -> Result<UploadPackNegotiationRequest> {
4563    let mut frames = Vec::new();
4564    loop {
4565        let Some(frame) = read_pkt_line_frame(reader)? else {
4566            return Err(GitError::InvalidFormat(
4567                "pkt-line stream ended before upload-pack negotiation terminator".into(),
4568            ));
4569        };
4570        let done = match &frame {
4571            PktLineFrame::Flush => true,
4572            PktLineFrame::Data(payload) => trim_trailing_lf(payload) == b"done",
4573            _ => false,
4574        };
4575        frames.push(frame);
4576        if done {
4577            return parse_upload_pack_negotiation_request(format, &frames);
4578        }
4579    }
4580}
4581
4582pub fn write_upload_pack_negotiation_request(
4583    writer: &mut impl Write,
4584    request: &UploadPackNegotiationRequest,
4585) -> Result<()> {
4586    for oid in &request.haves {
4587        write_pkt_line_payload(writer, &line_from_str(&format!("have {oid}")))?;
4588    }
4589    if request.done {
4590        write_pkt_line_payload(writer, b"done\n")?;
4591    } else {
4592        writer.write_all(b"0000")?;
4593    }
4594    Ok(())
4595}
4596
4597pub fn parse_upload_pack_acknowledgment(
4598    format: ObjectFormat,
4599    payload: &[u8],
4600) -> Result<UploadPackAcknowledgment> {
4601    let text = parse_protocol_v2_line_text("upload-pack acknowledgment", payload)?;
4602    if text == "NAK" {
4603        return Ok(UploadPackAcknowledgment::Nak);
4604    }
4605    let Some(rest) = text.strip_prefix("ACK ") else {
4606        return Err(GitError::InvalidFormat(format!(
4607            "unsupported upload-pack acknowledgment {text}"
4608        )));
4609    };
4610    let mut fields = rest.split(' ');
4611    let oid = fields
4612        .next()
4613        .ok_or_else(|| GitError::InvalidFormat("upload-pack ACK missing object id".into()))?;
4614    validate_protocol_v2_token("upload-pack ACK", oid)?;
4615    let status = match fields.next() {
4616        None => None,
4617        Some("continue") => Some(UploadPackAckStatus::Continue),
4618        Some("common") => Some(UploadPackAckStatus::Common),
4619        Some("ready") => Some(UploadPackAckStatus::Ready),
4620        Some(other) => {
4621            return Err(GitError::InvalidFormat(format!(
4622                "unsupported upload-pack ACK status {other}"
4623            )));
4624        }
4625    };
4626    if fields.next().is_some() {
4627        return Err(GitError::InvalidFormat(
4628            "upload-pack ACK has too many fields".into(),
4629        ));
4630    }
4631    Ok(UploadPackAcknowledgment::Ack {
4632        oid: ObjectId::from_hex(format, oid)?,
4633        status,
4634    })
4635}
4636
4637pub fn encode_upload_pack_acknowledgment(
4638    acknowledgment: &UploadPackAcknowledgment,
4639) -> Result<Vec<u8>> {
4640    let line = match acknowledgment {
4641        UploadPackAcknowledgment::Nak => "NAK".to_string(),
4642        UploadPackAcknowledgment::Ack { oid, status } => {
4643            let mut line = format!("ACK {oid}");
4644            if let Some(status) = status {
4645                line.push(' ');
4646                line.push_str(match status {
4647                    UploadPackAckStatus::Continue => "continue",
4648                    UploadPackAckStatus::Common => "common",
4649                    UploadPackAckStatus::Ready => "ready",
4650                });
4651            }
4652            line
4653        }
4654    };
4655    Ok(line_from_str(&line))
4656}
4657
4658pub fn read_upload_pack_acknowledgment(
4659    format: ObjectFormat,
4660    reader: &mut impl Read,
4661) -> Result<UploadPackAcknowledgment> {
4662    let Some(frame) = read_pkt_line_frame(reader)? else {
4663        return Err(GitError::InvalidFormat(
4664            "pkt-line stream ended before upload-pack acknowledgment".into(),
4665        ));
4666    };
4667    match frame {
4668        PktLineFrame::Data(payload) => parse_upload_pack_acknowledgment(format, &payload),
4669        _ => Err(GitError::InvalidFormat(
4670            "upload-pack acknowledgment must be a data packet".into(),
4671        )),
4672    }
4673}
4674
4675pub fn write_upload_pack_acknowledgment(
4676    writer: &mut impl Write,
4677    acknowledgment: &UploadPackAcknowledgment,
4678) -> Result<()> {
4679    write_pkt_line_payload(writer, &encode_upload_pack_acknowledgment(acknowledgment)?)
4680}
4681
4682pub fn parse_upload_pack_packfile_response(
4683    format: ObjectFormat,
4684    frames: &[PktLineFrame],
4685) -> Result<UploadPackPackfileResponse> {
4686    let mut response = UploadPackPackfileResponse::default();
4687    let mut in_sideband = false;
4688    let mut saw_flush = false;
4689    for (idx, frame) in frames.iter().enumerate() {
4690        match frame {
4691            PktLineFrame::Data(payload) if !saw_flush => {
4692                if !in_sideband
4693                    && (trim_trailing_lf(payload) == b"NAK" || payload.starts_with(b"ACK "))
4694                {
4695                    response
4696                        .acknowledgments
4697                        .push(parse_upload_pack_acknowledgment(format, payload)?);
4698                    continue;
4699                }
4700                in_sideband = true;
4701                response.sideband.push(parse_sideband_packet(payload)?);
4702            }
4703            PktLineFrame::Data(_) => {
4704                return Err(GitError::InvalidFormat(
4705                    "upload-pack packfile response has data after flush".into(),
4706                ));
4707            }
4708            PktLineFrame::Flush => {
4709                saw_flush = true;
4710                if idx + 1 != frames.len() {
4711                    return Err(GitError::InvalidFormat(
4712                        "upload-pack packfile response has frames after flush".into(),
4713                    ));
4714                }
4715            }
4716            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4717                return Err(GitError::InvalidFormat(
4718                    "upload-pack packfile response contains a non-flush control packet".into(),
4719                ));
4720            }
4721        }
4722    }
4723    if !saw_flush {
4724        return Err(GitError::InvalidFormat(
4725            "upload-pack packfile response missing flush".into(),
4726        ));
4727    }
4728    Ok(response)
4729}
4730
4731pub fn encode_upload_pack_packfile_response(
4732    response: &UploadPackPackfileResponse,
4733) -> Result<Vec<PktLineFrame>> {
4734    let mut frames = Vec::new();
4735    for acknowledgment in &response.acknowledgments {
4736        frames.push(PktLineFrame::data(encode_upload_pack_acknowledgment(
4737            acknowledgment,
4738        )?)?);
4739    }
4740    for packet in &response.sideband {
4741        frames.push(PktLineFrame::data(encode_sideband_packet(packet)?)?);
4742    }
4743    frames.push(PktLineFrame::Flush);
4744    Ok(frames)
4745}
4746
4747pub fn read_upload_pack_packfile_response(
4748    format: ObjectFormat,
4749    reader: &mut impl Read,
4750) -> Result<UploadPackPackfileResponse> {
4751    let frames = read_pkt_line_frames_until_flush(reader)?;
4752    parse_upload_pack_packfile_response(format, &frames)
4753}
4754
4755pub fn write_upload_pack_packfile_response(
4756    writer: &mut impl Write,
4757    response: &UploadPackPackfileResponse,
4758) -> Result<()> {
4759    for acknowledgment in &response.acknowledgments {
4760        write_upload_pack_acknowledgment(writer, acknowledgment)?;
4761    }
4762    for packet in &response.sideband {
4763        write_sideband_packet(writer, packet)?;
4764    }
4765    writer.write_all(b"0000")?;
4766    Ok(())
4767}
4768
4769pub fn demux_upload_pack_packfile_response(
4770    response: &UploadPackPackfileResponse,
4771) -> Result<SideBandDemux> {
4772    demux_sideband_packets(&response.sideband)
4773}
4774
4775pub fn parse_upload_pack_raw_packfile_response(
4776    format: ObjectFormat,
4777    input: &[u8],
4778) -> Result<UploadPackRawPackfileResponse> {
4779    let mut response = UploadPackRawPackfileResponse::default();
4780    let mut offset = 0usize;
4781    while offset < input.len() {
4782        match PktLineFrame::parse(&input[offset..]) {
4783            Ok((PktLineFrame::Data(payload), consumed)) => {
4784                let trimmed = trim_trailing_lf(&payload);
4785                if trimmed == b"NAK" || trimmed.starts_with(b"ACK ") {
4786                    response
4787                        .acknowledgments
4788                        .push(parse_upload_pack_acknowledgment(format, &payload)?);
4789                    offset += consumed;
4790                    continue;
4791                }
4792                return Err(GitError::InvalidFormat(
4793                    "upload-pack raw packfile response has non-ack pkt-line before packfile".into(),
4794                ));
4795            }
4796            Ok((PktLineFrame::Flush | PktLineFrame::Delimiter | PktLineFrame::ResponseEnd, _)) => {
4797                return Err(GitError::InvalidFormat(
4798                    "upload-pack raw packfile response contains a control packet".into(),
4799                ));
4800            }
4801            Err(_) if input[offset..].starts_with(b"PACK") => break,
4802            Err(err) => return Err(err),
4803        }
4804    }
4805    response.packfile = input[offset..].to_vec();
4806    if response.packfile.is_empty() {
4807        return Err(GitError::InvalidFormat(
4808            "upload-pack raw packfile response missing packfile".into(),
4809        ));
4810    }
4811    if !response.packfile.starts_with(b"PACK") {
4812        return Err(GitError::InvalidFormat(
4813            "upload-pack raw packfile response packfile must start with PACK".into(),
4814        ));
4815    }
4816    Ok(response)
4817}
4818
4819pub fn encode_upload_pack_raw_packfile_response(
4820    response: &UploadPackRawPackfileResponse,
4821) -> Result<Vec<u8>> {
4822    if response.packfile.is_empty() {
4823        return Err(GitError::InvalidFormat(
4824            "upload-pack raw packfile response missing packfile".into(),
4825        ));
4826    }
4827    if !response.packfile.starts_with(b"PACK") {
4828        return Err(GitError::InvalidFormat(
4829            "upload-pack raw packfile response packfile must start with PACK".into(),
4830        ));
4831    }
4832    let mut out = Vec::new();
4833    for acknowledgment in &response.acknowledgments {
4834        write_pkt_line_payload(
4835            &mut out,
4836            &encode_upload_pack_acknowledgment(acknowledgment)?,
4837        )?;
4838    }
4839    out.extend_from_slice(&response.packfile);
4840    Ok(out)
4841}
4842
4843pub fn read_upload_pack_raw_packfile_response(
4844    format: ObjectFormat,
4845    reader: &mut impl Read,
4846) -> Result<UploadPackRawPackfileResponse> {
4847    let mut input = Vec::new();
4848    reader.read_to_end(&mut input)?;
4849    parse_upload_pack_raw_packfile_response(format, &input)
4850}
4851
4852pub fn write_upload_pack_raw_packfile_response(
4853    writer: &mut impl Write,
4854    response: &UploadPackRawPackfileResponse,
4855) -> Result<()> {
4856    if response.packfile.is_empty() {
4857        return Err(GitError::InvalidFormat(
4858            "upload-pack raw packfile response missing packfile".into(),
4859        ));
4860    }
4861    if !response.packfile.starts_with(b"PACK") {
4862        return Err(GitError::InvalidFormat(
4863            "upload-pack raw packfile response packfile must start with PACK".into(),
4864        ));
4865    }
4866    for acknowledgment in &response.acknowledgments {
4867        write_upload_pack_acknowledgment(writer, acknowledgment)?;
4868    }
4869    writer.write_all(&response.packfile)?;
4870    Ok(())
4871}
4872
4873/// Parse the smart-HTTP/SSH v0 *shallow-info* section that precedes the packfile
4874/// when the upload-pack request carried `shallow`/`deepen`/`deepen-since`/
4875/// `deepen-not` arguments.
4876///
4877/// The section is zero or more `shallow <oid>` / `unshallow <oid>` pkt-lines
4878/// terminated by a flush-pkt; git always emits it (even empty, as a bare flush)
4879/// when the request was a deepen request, and never emits it otherwise. Returns
4880/// the parsed entries and the number of bytes consumed (through the flush) so the
4881/// caller can continue parsing the trailing acknowledgments + packfile from
4882/// `&input[consumed..]` (see [`parse_upload_pack_shallow_info_and_raw_packfile_response`]).
4883pub fn parse_upload_pack_shallow_info_section(
4884    format: ObjectFormat,
4885    input: &[u8],
4886) -> Result<(Vec<ProtocolV2FetchShallowInfo>, usize)> {
4887    let mut entries = Vec::new();
4888    let mut offset = 0usize;
4889    loop {
4890        let (frame, consumed) = PktLineFrame::parse(&input[offset..])?;
4891        offset += consumed;
4892        match frame {
4893            PktLineFrame::Data(payload) => {
4894                entries.push(parse_fetch_shallow_info(format, &payload)?)
4895            }
4896            PktLineFrame::Flush => return Ok((entries, offset)),
4897            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4898                return Err(GitError::InvalidFormat(
4899                    "upload-pack shallow-info section contains a non-flush control packet".into(),
4900                ));
4901            }
4902        }
4903    }
4904}
4905
4906/// Parse a raw upload-pack response that begins with a *shallow-info* section,
4907/// i.e. the response to a deepen request.
4908///
4909/// This is [`parse_upload_pack_raw_packfile_response`] preceded by the
4910/// shallow-info section ([`parse_upload_pack_shallow_info_section`]): it returns
4911/// the `shallow`/`unshallow` entries the server reported alongside the parsed
4912/// acknowledgments + raw packfile. Use it only when the request carried a
4913/// `shallow`/`deepen`/`deepen-since`/`deepen-not` argument; for a plain (non-deepen)
4914/// request the response has no leading shallow-info section and
4915/// [`parse_upload_pack_raw_packfile_response`] must be used instead.
4916pub fn parse_upload_pack_shallow_info_and_raw_packfile_response(
4917    format: ObjectFormat,
4918    input: &[u8],
4919) -> Result<(
4920    Vec<ProtocolV2FetchShallowInfo>,
4921    UploadPackRawPackfileResponse,
4922)> {
4923    let (shallow, consumed) = parse_upload_pack_shallow_info_section(format, input)?;
4924    let response = parse_upload_pack_raw_packfile_response(format, &input[consumed..])?;
4925    Ok((shallow, response))
4926}
4927
4928/// Read a raw upload-pack response that begins with a *shallow-info* section from
4929/// `reader`, returning the `shallow`/`unshallow` entries and the parsed
4930/// acknowledgments + raw packfile.
4931///
4932/// The reader counterpart of
4933/// [`parse_upload_pack_shallow_info_and_raw_packfile_response`]; see it for when
4934/// this applies.
4935pub fn read_upload_pack_shallow_info_and_raw_packfile_response(
4936    format: ObjectFormat,
4937    reader: &mut impl Read,
4938) -> Result<(
4939    Vec<ProtocolV2FetchShallowInfo>,
4940    UploadPackRawPackfileResponse,
4941)> {
4942    let mut input = Vec::new();
4943    reader.read_to_end(&mut input)?;
4944    parse_upload_pack_shallow_info_and_raw_packfile_response(format, &input)
4945}
4946
4947pub fn parse_receive_pack_request(
4948    format: ObjectFormat,
4949    frames: &[PktLineFrame],
4950) -> Result<ReceivePackRequest> {
4951    let mut request = ReceivePackRequest::default();
4952    let mut saw_command = false;
4953    let mut saw_flush = false;
4954    for (idx, frame) in frames.iter().enumerate() {
4955        match frame {
4956            PktLineFrame::Data(payload) if !saw_flush => {
4957                let payload = trim_trailing_lf(payload);
4958                if payload.is_empty() {
4959                    return Err(GitError::InvalidFormat(
4960                        "receive-pack request line is empty".into(),
4961                    ));
4962                }
4963                if let Some(shallow) = payload.strip_prefix(b"shallow ") {
4964                    if saw_command {
4965                        return Err(GitError::InvalidFormat(
4966                            "receive-pack request has shallow after commands".into(),
4967                        ));
4968                    }
4969                    let shallow = std::str::from_utf8(shallow)
4970                        .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
4971                    validate_protocol_v2_token("receive-pack shallow", shallow)?;
4972                    request.shallow.push(ObjectId::from_hex(format, shallow)?);
4973                    continue;
4974                }
4975
4976                let (command, capabilities) = match payload.iter().position(|byte| *byte == 0) {
4977                    Some(nul) if !saw_command => (
4978                        &payload[..nul],
4979                        Some(parse_capabilities(&payload[nul + 1..])?),
4980                    ),
4981                    Some(_) => {
4982                        return Err(GitError::InvalidFormat(
4983                            "receive-pack capabilities must appear on the first command".into(),
4984                        ));
4985                    }
4986                    None => (payload, None),
4987                };
4988                let command = parse_receive_pack_command(format, command)?;
4989                if let Some(capabilities) = capabilities {
4990                    request.capabilities = capabilities;
4991                }
4992                request.commands.push(command);
4993                saw_command = true;
4994            }
4995            PktLineFrame::Data(_) => {
4996                return Err(GitError::InvalidFormat(
4997                    "receive-pack request has data after flush".into(),
4998                ));
4999            }
5000            PktLineFrame::Flush => {
5001                saw_flush = true;
5002                if idx + 1 != frames.len() {
5003                    return Err(GitError::InvalidFormat(
5004                        "receive-pack request has frames after flush".into(),
5005                    ));
5006                }
5007            }
5008            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5009                return Err(GitError::InvalidFormat(
5010                    "receive-pack request contains a non-flush control packet".into(),
5011                ));
5012            }
5013        }
5014    }
5015    if !saw_flush {
5016        return Err(GitError::InvalidFormat(
5017            "receive-pack request missing flush".into(),
5018        ));
5019    }
5020    if !request.shallow.is_empty() && request.commands.is_empty() {
5021        return Err(GitError::InvalidFormat(
5022            "receive-pack request has shallow lines without commands".into(),
5023        ));
5024    }
5025    Ok(request)
5026}
5027
5028pub fn encode_receive_pack_request(request: &ReceivePackRequest) -> Result<Vec<PktLineFrame>> {
5029    if !request.shallow.is_empty() && request.commands.is_empty() {
5030        return Err(GitError::InvalidFormat(
5031            "receive-pack request has shallow lines without commands".into(),
5032        ));
5033    }
5034
5035    let mut frames = Vec::new();
5036    for oid in &request.shallow {
5037        frames.push(PktLineFrame::data(line_from_str(&format!(
5038            "shallow {oid}"
5039        )))?);
5040    }
5041    for (idx, command) in request.commands.iter().enumerate() {
5042        let mut payload = format_receive_pack_command(command)?;
5043        if idx == 0 && !request.capabilities.is_empty() {
5044            payload.push(0);
5045            payload.extend_from_slice(&encode_capabilities(&request.capabilities)?);
5046        }
5047        payload.push(b'\n');
5048        frames.push(PktLineFrame::data(payload)?);
5049    }
5050    frames.push(PktLineFrame::Flush);
5051    Ok(frames)
5052}
5053
5054pub fn read_receive_pack_request(
5055    format: ObjectFormat,
5056    reader: &mut impl Read,
5057) -> Result<ReceivePackRequest> {
5058    let frames = read_pkt_line_frames_until_flush(reader)?;
5059    parse_receive_pack_request(format, &frames)
5060}
5061
5062pub fn write_receive_pack_request(
5063    writer: &mut impl Write,
5064    request: &ReceivePackRequest,
5065) -> Result<()> {
5066    if !request.shallow.is_empty() && request.commands.is_empty() {
5067        return Err(GitError::InvalidFormat(
5068            "receive-pack request has shallow lines without commands".into(),
5069        ));
5070    }
5071
5072    for oid in &request.shallow {
5073        write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
5074    }
5075    for (idx, command) in request.commands.iter().enumerate() {
5076        let mut payload = format_receive_pack_command(command)?;
5077        if idx == 0 && !request.capabilities.is_empty() {
5078            payload.push(0);
5079            payload.extend_from_slice(&encode_capabilities(&request.capabilities)?);
5080        }
5081        payload.push(b'\n');
5082        write_pkt_line_payload(writer, &payload)?;
5083    }
5084    writer.write_all(b"0000")?;
5085    Ok(())
5086}
5087
5088pub fn parse_receive_pack_push_request(
5089    format: ObjectFormat,
5090    input: &[u8],
5091    has_push_options: bool,
5092) -> Result<ReceivePackPushRequest> {
5093    let (command_frames, consumed) = parse_pkt_line_frames_until_flush_from(input)?;
5094    let commands = parse_receive_pack_request(format, &command_frames)?;
5095    let mut offset = consumed;
5096    let push_options = if has_push_options {
5097        let (push_option_frames, consumed) =
5098            parse_pkt_line_frames_until_flush_from(&input[offset..])?;
5099        offset += consumed;
5100        Some(parse_receive_pack_push_options(&push_option_frames)?)
5101    } else {
5102        None
5103    };
5104    Ok(ReceivePackPushRequest {
5105        commands,
5106        push_options,
5107        packfile: input[offset..].to_vec(),
5108    })
5109}
5110
5111pub fn encode_receive_pack_push_request(request: &ReceivePackPushRequest) -> Result<Vec<u8>> {
5112    let mut out = Vec::new();
5113    write_receive_pack_request(&mut out, &request.commands)?;
5114    if let Some(push_options) = &request.push_options {
5115        write_receive_pack_push_options(&mut out, push_options)?;
5116    }
5117    out.extend_from_slice(&request.packfile);
5118    Ok(out)
5119}
5120
5121pub fn read_receive_pack_push_request(
5122    format: ObjectFormat,
5123    reader: &mut impl Read,
5124    has_push_options: bool,
5125) -> Result<ReceivePackPushRequest> {
5126    let commands = read_receive_pack_request(format, reader)?;
5127    let push_options = if has_push_options {
5128        Some(read_receive_pack_push_options(reader)?)
5129    } else {
5130        None
5131    };
5132    let mut packfile = Vec::new();
5133    reader.read_to_end(&mut packfile)?;
5134    Ok(ReceivePackPushRequest {
5135        commands,
5136        push_options,
5137        packfile,
5138    })
5139}
5140
5141pub fn write_receive_pack_push_request(
5142    writer: &mut impl Write,
5143    request: &ReceivePackPushRequest,
5144) -> Result<()> {
5145    write_receive_pack_request(writer, &request.commands)?;
5146    if let Some(push_options) = &request.push_options {
5147        write_receive_pack_push_options(writer, push_options)?;
5148    }
5149    writer.write_all(&request.packfile)?;
5150    Ok(())
5151}
5152
5153pub fn parse_receive_pack_features(capabilities: &[Capability]) -> Result<ReceivePackFeatures> {
5154    let mut features = ReceivePackFeatures::default();
5155    for capability in capabilities {
5156        match capability.name.as_str() {
5157            "report-status" => {
5158                reject_capability_value("receive-pack report-status", capability)?;
5159                if features.report_status {
5160                    return Err(GitError::InvalidFormat(
5161                        "receive-pack has duplicate report-status capability".into(),
5162                    ));
5163                }
5164                features.report_status = true;
5165            }
5166            "report-status-v2" => {
5167                reject_capability_value("receive-pack report-status-v2", capability)?;
5168                if features.report_status_v2 {
5169                    return Err(GitError::InvalidFormat(
5170                        "receive-pack has duplicate report-status-v2 capability".into(),
5171                    ));
5172                }
5173                features.report_status_v2 = true;
5174            }
5175            "delete-refs" => {
5176                reject_capability_value("receive-pack delete-refs", capability)?;
5177                if features.delete_refs {
5178                    return Err(GitError::InvalidFormat(
5179                        "receive-pack has duplicate delete-refs capability".into(),
5180                    ));
5181                }
5182                features.delete_refs = true;
5183            }
5184            "ofs-delta" => {
5185                reject_capability_value("receive-pack ofs-delta", capability)?;
5186                if features.ofs_delta {
5187                    return Err(GitError::InvalidFormat(
5188                        "receive-pack has duplicate ofs-delta capability".into(),
5189                    ));
5190                }
5191                features.ofs_delta = true;
5192            }
5193            "atomic" => {
5194                reject_capability_value("receive-pack atomic", capability)?;
5195                if features.atomic {
5196                    return Err(GitError::InvalidFormat(
5197                        "receive-pack has duplicate atomic capability".into(),
5198                    ));
5199                }
5200                features.atomic = true;
5201            }
5202            "push-options" => {
5203                reject_capability_value("receive-pack push-options", capability)?;
5204                if features.push_options {
5205                    return Err(GitError::InvalidFormat(
5206                        "receive-pack has duplicate push-options capability".into(),
5207                    ));
5208                }
5209                features.push_options = true;
5210            }
5211            "side-band-64k" => {
5212                reject_capability_value("receive-pack side-band-64k", capability)?;
5213                if features.side_band_64k {
5214                    return Err(GitError::InvalidFormat(
5215                        "receive-pack has duplicate side-band-64k capability".into(),
5216                    ));
5217                }
5218                features.side_band_64k = true;
5219            }
5220            "quiet" => {
5221                reject_capability_value("receive-pack quiet", capability)?;
5222                if features.quiet {
5223                    return Err(GitError::InvalidFormat(
5224                        "receive-pack has duplicate quiet capability".into(),
5225                    ));
5226                }
5227                features.quiet = true;
5228            }
5229            "no-thin" => {
5230                reject_capability_value("receive-pack no-thin", capability)?;
5231                if features.no_thin {
5232                    return Err(GitError::InvalidFormat(
5233                        "receive-pack has duplicate no-thin capability".into(),
5234                    ));
5235                }
5236                features.no_thin = true;
5237            }
5238            "agent" => {
5239                let Some(agent) = &capability.value else {
5240                    return Err(GitError::InvalidFormat(
5241                        "receive-pack agent capability is missing value".into(),
5242                    ));
5243                };
5244                if features.agent.is_some() {
5245                    return Err(GitError::InvalidFormat(
5246                        "receive-pack has duplicate agent capability".into(),
5247                    ));
5248                }
5249                validate_capability_field("receive-pack agent", agent)?;
5250                features.agent = Some(agent.clone());
5251            }
5252            "object-format" => {
5253                let Some(format) = &capability.value else {
5254                    return Err(GitError::InvalidFormat(
5255                        "receive-pack object-format capability is missing value".into(),
5256                    ));
5257                };
5258                if features.object_format.is_some() {
5259                    return Err(GitError::InvalidFormat(
5260                        "receive-pack has duplicate object-format capability".into(),
5261                    ));
5262                }
5263                validate_capability_field("receive-pack object-format", format)?;
5264                features.object_format = Some(format.parse()?);
5265            }
5266            _ => {
5267                encode_capabilities(std::slice::from_ref(capability))?;
5268                if features
5269                    .unknown
5270                    .iter()
5271                    .any(|known| known.name == capability.name)
5272                {
5273                    return Err(GitError::InvalidFormat(format!(
5274                        "receive-pack has duplicate {} capability",
5275                        capability.name
5276                    )));
5277                }
5278                features.unknown.push(capability.clone());
5279            }
5280        }
5281    }
5282    Ok(features)
5283}
5284
5285pub fn encode_receive_pack_features(features: &ReceivePackFeatures) -> Result<Vec<Capability>> {
5286    let mut capabilities = Vec::new();
5287    if features.report_status {
5288        capabilities.push(Capability {
5289            name: "report-status".into(),
5290            value: None,
5291        });
5292    }
5293    if features.report_status_v2 {
5294        capabilities.push(Capability {
5295            name: "report-status-v2".into(),
5296            value: None,
5297        });
5298    }
5299    if features.delete_refs {
5300        capabilities.push(Capability {
5301            name: "delete-refs".into(),
5302            value: None,
5303        });
5304    }
5305    if features.ofs_delta {
5306        capabilities.push(Capability {
5307            name: "ofs-delta".into(),
5308            value: None,
5309        });
5310    }
5311    if features.atomic {
5312        capabilities.push(Capability {
5313            name: "atomic".into(),
5314            value: None,
5315        });
5316    }
5317    if features.push_options {
5318        capabilities.push(Capability {
5319            name: "push-options".into(),
5320            value: None,
5321        });
5322    }
5323    if features.side_band_64k {
5324        capabilities.push(Capability {
5325            name: "side-band-64k".into(),
5326            value: None,
5327        });
5328    }
5329    if features.quiet {
5330        capabilities.push(Capability {
5331            name: "quiet".into(),
5332            value: None,
5333        });
5334    }
5335    if features.no_thin {
5336        capabilities.push(Capability {
5337            name: "no-thin".into(),
5338            value: None,
5339        });
5340    }
5341    if let Some(agent) = &features.agent {
5342        validate_capability_field("receive-pack agent", agent)?;
5343        capabilities.push(Capability {
5344            name: "agent".into(),
5345            value: Some(agent.clone()),
5346        });
5347    }
5348    if let Some(format) = features.object_format {
5349        capabilities.push(Capability {
5350            name: "object-format".into(),
5351            value: Some(format.name().into()),
5352        });
5353    }
5354    for capability in &features.unknown {
5355        if is_known_receive_pack_capability(&capability.name) {
5356            return Err(GitError::InvalidFormat(format!(
5357                "receive-pack unknown capability duplicates known capability {}",
5358                capability.name
5359            )));
5360        }
5361        encode_capabilities(std::slice::from_ref(capability))?;
5362        capabilities.push(capability.clone());
5363    }
5364    Ok(capabilities)
5365}
5366
5367pub fn validate_receive_pack_push_request_features(
5368    features: &ReceivePackFeatures,
5369    request: &ReceivePackPushRequest,
5370) -> Result<()> {
5371    for capability in &request.commands.capabilities {
5372        if matches!(
5373            capability.name.as_str(),
5374            "report-status"
5375                | "report-status-v2"
5376                | "delete-refs"
5377                | "ofs-delta"
5378                | "atomic"
5379                | "push-options"
5380                | "side-band-64k"
5381                | "quiet"
5382                | "no-thin"
5383        ) {
5384            reject_capability_value("receive-pack request capability", capability)?;
5385        }
5386        match capability.name.as_str() {
5387            "report-status" if !features.report_status => {
5388                return Err(GitError::InvalidFormat(
5389                    "receive-pack request uses report-status without advertised capability".into(),
5390                ));
5391            }
5392            "report-status-v2" if !features.report_status_v2 => {
5393                return Err(GitError::InvalidFormat(
5394                    "receive-pack request uses report-status-v2 without advertised capability"
5395                        .into(),
5396                ));
5397            }
5398            "delete-refs" if !features.delete_refs => {
5399                return Err(GitError::InvalidFormat(
5400                    "receive-pack request uses delete-refs without advertised capability".into(),
5401                ));
5402            }
5403            "ofs-delta" if !features.ofs_delta => {
5404                return Err(GitError::InvalidFormat(
5405                    "receive-pack request uses ofs-delta without advertised capability".into(),
5406                ));
5407            }
5408            "atomic" if !features.atomic => {
5409                return Err(GitError::InvalidFormat(
5410                    "receive-pack request uses atomic without advertised capability".into(),
5411                ));
5412            }
5413            "push-options" if !features.push_options => {
5414                return Err(GitError::InvalidFormat(
5415                    "receive-pack request uses push-options without advertised capability".into(),
5416                ));
5417            }
5418            "side-band-64k" if !features.side_band_64k => {
5419                return Err(GitError::InvalidFormat(
5420                    "receive-pack request uses side-band-64k without advertised capability".into(),
5421                ));
5422            }
5423            "quiet" if !features.quiet => {
5424                return Err(GitError::InvalidFormat(
5425                    "receive-pack request uses quiet without advertised capability".into(),
5426                ));
5427            }
5428            "no-thin" => {
5429                return Err(GitError::InvalidFormat(
5430                    "receive-pack request must not request no-thin".into(),
5431                ));
5432            }
5433            "agent" => {
5434                validate_capability_field(
5435                    "receive-pack request agent",
5436                    capability.value.as_deref().unwrap_or_default(),
5437                )?;
5438            }
5439            "object-format" => {
5440                let Some(value) = &capability.value else {
5441                    return Err(GitError::InvalidFormat(
5442                        "receive-pack request object-format capability is missing value".into(),
5443                    ));
5444                };
5445                let requested_format: ObjectFormat = value.parse()?;
5446                if features.object_format != Some(requested_format) {
5447                    return Err(GitError::InvalidFormat(
5448                        "receive-pack request object-format was not advertised".into(),
5449                    ));
5450                }
5451            }
5452            name if is_known_receive_pack_capability(name) => {}
5453            _ => {
5454                if !features
5455                    .unknown
5456                    .iter()
5457                    .any(|advertised| advertised.name == capability.name)
5458                {
5459                    return Err(GitError::InvalidFormat(format!(
5460                        "receive-pack request uses unadvertised capability {}",
5461                        capability.name
5462                    )));
5463                }
5464            }
5465        }
5466    }
5467
5468    let requested_push_options = request
5469        .commands
5470        .capabilities
5471        .iter()
5472        .any(|capability| capability.name == "push-options");
5473    match (requested_push_options, &request.push_options) {
5474        (true, Some(_)) => {}
5475        (true, None) => {
5476            return Err(GitError::InvalidFormat(
5477                "receive-pack request uses push-options without push-options section".into(),
5478            ));
5479        }
5480        (false, Some(_)) => {
5481            return Err(GitError::InvalidFormat(
5482                "receive-pack request has push-options section without requested capability".into(),
5483            ));
5484        }
5485        (false, None) => {}
5486    }
5487
5488    let has_delete = request
5489        .commands
5490        .commands
5491        .iter()
5492        .any(is_receive_pack_delete_command);
5493    if has_delete && !features.delete_refs {
5494        return Err(GitError::InvalidFormat(
5495            "receive-pack request deletes refs without advertised delete-refs capability".into(),
5496        ));
5497    }
5498
5499    let has_update_or_create = request
5500        .commands
5501        .commands
5502        .iter()
5503        .any(|command| !is_receive_pack_delete_command(command));
5504    if has_update_or_create && request.packfile.is_empty() {
5505        return Err(GitError::InvalidFormat(
5506            "receive-pack request with create/update commands is missing packfile".into(),
5507        ));
5508    }
5509    if !has_update_or_create && !request.packfile.is_empty() {
5510        return Err(GitError::InvalidFormat(
5511            "receive-pack delete-only request must not include packfile".into(),
5512        ));
5513    }
5514    Ok(())
5515}
5516
5517pub fn apply_receive_pack_push_request<R, I, C, U, D>(
5518    features: &ReceivePackFeatures,
5519    request: &ReceivePackPushRequest,
5520    mut read_ref: R,
5521    mut install_pack: I,
5522    mut contains_object: C,
5523    mut apply_updates: U,
5524    mut delete_ref: D,
5525) -> Result<ReceivePackReportStatus>
5526where
5527    R: FnMut(&str) -> Result<Option<ObjectId>>,
5528    I: FnMut(&[u8]) -> Result<()>,
5529    C: FnMut(&ObjectId) -> Result<bool>,
5530    U: FnMut(&[ReceivePackCommand]) -> Result<()>,
5531    D: FnMut(&ReceivePackCommand) -> Result<()>,
5532{
5533    validate_receive_pack_push_request_features(features, request)?;
5534
5535    for command in request
5536        .commands
5537        .commands
5538        .iter()
5539        .filter(|command| is_receive_pack_delete_command(command))
5540    {
5541        if !command.old_id.is_null() && read_ref(&command.name)? != Some(command.old_id.clone()) {
5542            return Err(GitError::Transaction(format!(
5543                "expected ref {} to match",
5544                command.name
5545            )));
5546        }
5547    }
5548
5549    let updates = request
5550        .commands
5551        .commands
5552        .iter()
5553        .filter(|command| !is_receive_pack_delete_command(command))
5554        .cloned()
5555        .collect::<Vec<_>>();
5556    if !updates.is_empty() {
5557        install_pack(&request.packfile)?;
5558        for command in &updates {
5559            if !contains_object(&command.new_id)? {
5560                return Err(GitError::InvalidObject(format!(
5561                    "receive-pack packfile did not provide {}",
5562                    command.new_id
5563                )));
5564            }
5565        }
5566        apply_updates(&updates)?;
5567    }
5568
5569    for command in request
5570        .commands
5571        .commands
5572        .iter()
5573        .filter(|command| is_receive_pack_delete_command(command))
5574    {
5575        delete_ref(command)?;
5576    }
5577
5578    Ok(ReceivePackReportStatus {
5579        unpack: ReceivePackUnpackStatus::Ok,
5580        commands: request
5581            .commands
5582            .commands
5583            .iter()
5584            .map(|command| ReceivePackCommandStatus::Ok {
5585                name: command.name.clone(),
5586            })
5587            .collect(),
5588    })
5589}
5590
5591pub fn parse_receive_pack_report_status(
5592    frames: &[PktLineFrame],
5593) -> Result<ReceivePackReportStatus> {
5594    let Some((first, rest)) = frames.split_first() else {
5595        return Err(GitError::InvalidFormat(
5596            "receive-pack report-status is empty".into(),
5597        ));
5598    };
5599    let PktLineFrame::Data(payload) = first else {
5600        return Err(GitError::InvalidFormat(
5601            "receive-pack report-status must start with unpack status".into(),
5602        ));
5603    };
5604    let unpack = parse_receive_pack_unpack_status(payload)?;
5605
5606    let mut commands = Vec::new();
5607    let mut saw_flush = false;
5608    for (idx, frame) in rest.iter().enumerate() {
5609        match frame {
5610            PktLineFrame::Data(payload) if !saw_flush => {
5611                commands.push(parse_receive_pack_command_status(payload)?);
5612            }
5613            PktLineFrame::Data(_) => {
5614                return Err(GitError::InvalidFormat(
5615                    "receive-pack report-status has data after flush".into(),
5616                ));
5617            }
5618            PktLineFrame::Flush => {
5619                saw_flush = true;
5620                if idx + 1 != rest.len() {
5621                    return Err(GitError::InvalidFormat(
5622                        "receive-pack report-status has frames after flush".into(),
5623                    ));
5624                }
5625            }
5626            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5627                return Err(GitError::InvalidFormat(
5628                    "receive-pack report-status contains a non-flush control packet".into(),
5629                ));
5630            }
5631        }
5632    }
5633    if !saw_flush {
5634        return Err(GitError::InvalidFormat(
5635            "receive-pack report-status missing flush".into(),
5636        ));
5637    }
5638    Ok(ReceivePackReportStatus { unpack, commands })
5639}
5640
5641pub fn encode_receive_pack_report_status(
5642    report: &ReceivePackReportStatus,
5643) -> Result<Vec<PktLineFrame>> {
5644    let mut frames = Vec::new();
5645    frames.push(PktLineFrame::data(line_from_str(
5646        &format_receive_pack_unpack_status(&report.unpack)?,
5647    ))?);
5648    for command in &report.commands {
5649        frames.push(PktLineFrame::data(line_from_str(
5650            &format_receive_pack_command_status(command)?,
5651        ))?);
5652    }
5653    frames.push(PktLineFrame::Flush);
5654    Ok(frames)
5655}
5656
5657pub fn read_receive_pack_report_status(reader: &mut impl Read) -> Result<ReceivePackReportStatus> {
5658    let frames = read_pkt_line_frames_until_flush(reader)?;
5659    parse_receive_pack_report_status(&frames)
5660}
5661
5662pub fn write_receive_pack_report_status(
5663    writer: &mut impl Write,
5664    report: &ReceivePackReportStatus,
5665) -> Result<()> {
5666    write_pkt_line_payload(
5667        writer,
5668        &line_from_str(&format_receive_pack_unpack_status(&report.unpack)?),
5669    )?;
5670    for command in &report.commands {
5671        write_pkt_line_payload(
5672            writer,
5673            &line_from_str(&format_receive_pack_command_status(command)?),
5674        )?;
5675    }
5676    writer.write_all(b"0000")?;
5677    Ok(())
5678}
5679
5680pub fn parse_receive_pack_report_status_v2(
5681    format: ObjectFormat,
5682    frames: &[PktLineFrame],
5683) -> Result<ReceivePackReportStatusV2> {
5684    let Some((first, rest)) = frames.split_first() else {
5685        return Err(GitError::InvalidFormat(
5686            "receive-pack report-status-v2 is empty".into(),
5687        ));
5688    };
5689    let PktLineFrame::Data(payload) = first else {
5690        return Err(GitError::InvalidFormat(
5691            "receive-pack report-status-v2 must start with unpack status".into(),
5692        ));
5693    };
5694    let unpack = parse_receive_pack_unpack_status(payload)?;
5695
5696    let mut commands = Vec::new();
5697    let mut saw_flush = false;
5698    for (idx, frame) in rest.iter().enumerate() {
5699        match frame {
5700            PktLineFrame::Data(payload) if !saw_flush => {
5701                let text =
5702                    parse_protocol_v2_line_text("receive-pack report-status-v2 line", payload)?;
5703                if text.starts_with("option ") {
5704                    let Some(ReceivePackCommandStatusV2::Ok { options, .. }) = commands.last_mut()
5705                    else {
5706                        return Err(GitError::InvalidFormat(
5707                            "receive-pack report-status-v2 option without ok status".into(),
5708                        ));
5709                    };
5710                    parse_receive_pack_report_status_v2_option(format, text, options)?;
5711                } else {
5712                    commands.push(parse_receive_pack_command_status_v2(text)?);
5713                }
5714            }
5715            PktLineFrame::Data(_) => {
5716                return Err(GitError::InvalidFormat(
5717                    "receive-pack report-status-v2 has data after flush".into(),
5718                ));
5719            }
5720            PktLineFrame::Flush => {
5721                saw_flush = true;
5722                if idx + 1 != rest.len() {
5723                    return Err(GitError::InvalidFormat(
5724                        "receive-pack report-status-v2 has frames after flush".into(),
5725                    ));
5726                }
5727            }
5728            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5729                return Err(GitError::InvalidFormat(
5730                    "receive-pack report-status-v2 contains a non-flush control packet".into(),
5731                ));
5732            }
5733        }
5734    }
5735    if !saw_flush {
5736        return Err(GitError::InvalidFormat(
5737            "receive-pack report-status-v2 missing flush".into(),
5738        ));
5739    }
5740    Ok(ReceivePackReportStatusV2 { unpack, commands })
5741}
5742
5743pub fn encode_receive_pack_report_status_v2(
5744    report: &ReceivePackReportStatusV2,
5745) -> Result<Vec<PktLineFrame>> {
5746    let mut frames = Vec::new();
5747    frames.push(PktLineFrame::data(line_from_str(
5748        &format_receive_pack_unpack_status(&report.unpack)?,
5749    ))?);
5750    for command in &report.commands {
5751        frames.push(PktLineFrame::data(line_from_str(
5752            &format_receive_pack_command_status_v2(command)?,
5753        ))?);
5754        if let ReceivePackCommandStatusV2::Ok { options, .. } = command {
5755            for option in format_receive_pack_report_status_v2_options(options)? {
5756                frames.push(PktLineFrame::data(line_from_str(&option))?);
5757            }
5758        }
5759    }
5760    frames.push(PktLineFrame::Flush);
5761    Ok(frames)
5762}
5763
5764pub fn read_receive_pack_report_status_v2(
5765    format: ObjectFormat,
5766    reader: &mut impl Read,
5767) -> Result<ReceivePackReportStatusV2> {
5768    let frames = read_pkt_line_frames_until_flush(reader)?;
5769    parse_receive_pack_report_status_v2(format, &frames)
5770}
5771
5772pub fn write_receive_pack_report_status_v2(
5773    writer: &mut impl Write,
5774    report: &ReceivePackReportStatusV2,
5775) -> Result<()> {
5776    write_pkt_line_payload(
5777        writer,
5778        &line_from_str(&format_receive_pack_unpack_status(&report.unpack)?),
5779    )?;
5780    for command in &report.commands {
5781        write_pkt_line_payload(
5782            writer,
5783            &line_from_str(&format_receive_pack_command_status_v2(command)?),
5784        )?;
5785        if let ReceivePackCommandStatusV2::Ok { options, .. } = command {
5786            for option in format_receive_pack_report_status_v2_options(options)? {
5787                write_pkt_line_payload(writer, &line_from_str(&option))?;
5788            }
5789        }
5790    }
5791    writer.write_all(b"0000")?;
5792    Ok(())
5793}
5794
5795pub fn parse_receive_pack_push_options(frames: &[PktLineFrame]) -> Result<Vec<String>> {
5796    let mut options = Vec::new();
5797    let mut saw_flush = false;
5798    for (idx, frame) in frames.iter().enumerate() {
5799        match frame {
5800            PktLineFrame::Data(payload) if !saw_flush => {
5801                let option = trim_trailing_lf(payload);
5802                validate_receive_pack_push_option(option)?;
5803                options.push(
5804                    std::str::from_utf8(option)
5805                        .map_err(|err| GitError::InvalidFormat(err.to_string()))?
5806                        .to_string(),
5807                );
5808            }
5809            PktLineFrame::Data(_) => {
5810                return Err(GitError::InvalidFormat(
5811                    "receive-pack push-options has data after flush".into(),
5812                ));
5813            }
5814            PktLineFrame::Flush => {
5815                saw_flush = true;
5816                if idx + 1 != frames.len() {
5817                    return Err(GitError::InvalidFormat(
5818                        "receive-pack push-options has frames after flush".into(),
5819                    ));
5820                }
5821            }
5822            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5823                return Err(GitError::InvalidFormat(
5824                    "receive-pack push-options contains a non-flush control packet".into(),
5825                ));
5826            }
5827        }
5828    }
5829    if !saw_flush {
5830        return Err(GitError::InvalidFormat(
5831            "receive-pack push-options missing flush".into(),
5832        ));
5833    }
5834    Ok(options)
5835}
5836
5837pub fn encode_receive_pack_push_options(options: &[String]) -> Result<Vec<PktLineFrame>> {
5838    let mut frames = Vec::new();
5839    for option in options {
5840        validate_receive_pack_push_option(option.as_bytes())?;
5841        let mut payload = option.as_bytes().to_vec();
5842        payload.push(b'\n');
5843        frames.push(PktLineFrame::data(payload)?);
5844    }
5845    frames.push(PktLineFrame::Flush);
5846    Ok(frames)
5847}
5848
5849pub fn read_receive_pack_push_options(reader: &mut impl Read) -> Result<Vec<String>> {
5850    let frames = read_pkt_line_frames_until_flush(reader)?;
5851    parse_receive_pack_push_options(&frames)
5852}
5853
5854pub fn write_receive_pack_push_options(writer: &mut impl Write, options: &[String]) -> Result<()> {
5855    for option in options {
5856        validate_receive_pack_push_option(option.as_bytes())?;
5857        let mut payload = option.as_bytes().to_vec();
5858        payload.push(b'\n');
5859        write_pkt_line_payload(writer, &payload)?;
5860    }
5861    writer.write_all(b"0000")?;
5862    Ok(())
5863}
5864
5865fn parse_receive_pack_command(format: ObjectFormat, payload: &[u8]) -> Result<ReceivePackCommand> {
5866    let text =
5867        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
5868    let mut fields = text.split(' ');
5869    let old_id = fields
5870        .next()
5871        .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing old id".into()))?;
5872    let new_id = fields
5873        .next()
5874        .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing new id".into()))?;
5875    let name = fields
5876        .next()
5877        .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing ref name".into()))?;
5878    if fields.next().is_some() {
5879        return Err(GitError::InvalidFormat(
5880            "receive-pack command has too many fields".into(),
5881        ));
5882    }
5883    validate_protocol_v2_token("receive-pack old id", old_id)?;
5884    validate_protocol_v2_token("receive-pack new id", new_id)?;
5885    validate_protocol_v2_token("receive-pack ref name", name)?;
5886    Ok(ReceivePackCommand {
5887        old_id: ObjectId::from_hex(format, old_id)?,
5888        new_id: ObjectId::from_hex(format, new_id)?,
5889        name: name.to_string(),
5890    })
5891}
5892
5893fn format_receive_pack_command(command: &ReceivePackCommand) -> Result<Vec<u8>> {
5894    if command.old_id.format() != command.new_id.format() {
5895        return Err(GitError::InvalidObjectId(
5896            "receive-pack command object formats do not match".into(),
5897        ));
5898    }
5899    validate_protocol_v2_token("receive-pack ref name", &command.name)?;
5900    Ok(format!("{} {} {}", command.old_id, command.new_id, command.name).into_bytes())
5901}
5902
5903fn set_upload_pack_flag(value: &mut bool, capability: &Capability) -> Result<()> {
5904    reject_capability_value("upload-pack capability", capability)?;
5905    if *value {
5906        return Err(GitError::InvalidFormat(format!(
5907            "upload-pack has duplicate {} capability",
5908            capability.name
5909        )));
5910    }
5911    *value = true;
5912    Ok(())
5913}
5914
5915fn push_upload_pack_flag(capabilities: &mut Vec<Capability>, name: &str, enabled: bool) {
5916    if enabled {
5917        capabilities.push(Capability {
5918            name: name.into(),
5919            value: None,
5920        });
5921    }
5922}
5923
5924fn is_known_upload_pack_capability(name: &str) -> bool {
5925    matches!(
5926        name,
5927        "multi_ack"
5928            | "multi_ack_detailed"
5929            | "no-done"
5930            | "thin-pack"
5931            | "side-band"
5932            | "side-band-64k"
5933            | "ofs-delta"
5934            | "shallow"
5935            | "deepen-since"
5936            | "deepen-not"
5937            | "include-tag"
5938            | "no-progress"
5939            | "allow-tip-sha1-in-want"
5940            | "allow-reachable-sha1-in-want"
5941            | "filter"
5942            | "agent"
5943            | "object-format"
5944            | "symref"
5945    )
5946}
5947
5948fn is_upload_pack_flag_capability(name: &str) -> bool {
5949    matches!(
5950        name,
5951        "multi_ack"
5952            | "multi_ack_detailed"
5953            | "no-done"
5954            | "thin-pack"
5955            | "side-band"
5956            | "side-band-64k"
5957            | "ofs-delta"
5958            | "shallow"
5959            | "deepen-since"
5960            | "deepen-not"
5961            | "include-tag"
5962            | "no-progress"
5963            | "allow-tip-sha1-in-want"
5964            | "allow-reachable-sha1-in-want"
5965            | "filter"
5966    )
5967}
5968
5969fn reject_capability_value(label: &str, capability: &Capability) -> Result<()> {
5970    if capability.value.is_some() {
5971        return Err(GitError::InvalidFormat(format!(
5972            "{label} must not have value"
5973        )));
5974    }
5975    Ok(())
5976}
5977
5978fn is_known_receive_pack_capability(name: &str) -> bool {
5979    matches!(
5980        name,
5981        "report-status"
5982            | "report-status-v2"
5983            | "delete-refs"
5984            | "ofs-delta"
5985            | "atomic"
5986            | "push-options"
5987            | "side-band-64k"
5988            | "quiet"
5989            | "no-thin"
5990            | "agent"
5991            | "object-format"
5992    )
5993}
5994
5995fn is_receive_pack_delete_command(command: &ReceivePackCommand) -> bool {
5996    command.new_id.is_null()
5997}
5998
5999fn parse_receive_pack_unpack_status(payload: &[u8]) -> Result<ReceivePackUnpackStatus> {
6000    let text = parse_protocol_v2_line_text("receive-pack unpack status", payload)?;
6001    if text == "unpack ok" {
6002        return Ok(ReceivePackUnpackStatus::Ok);
6003    }
6004    let Some(message) = text.strip_prefix("unpack ") else {
6005        return Err(GitError::InvalidFormat(format!(
6006            "unsupported receive-pack unpack status {text}"
6007        )));
6008    };
6009    validate_receive_pack_status_message("receive-pack unpack error", message)?;
6010    Ok(ReceivePackUnpackStatus::Error(message.to_string()))
6011}
6012
6013fn format_receive_pack_unpack_status(status: &ReceivePackUnpackStatus) -> Result<String> {
6014    match status {
6015        ReceivePackUnpackStatus::Ok => Ok("unpack ok".into()),
6016        ReceivePackUnpackStatus::Error(message) => {
6017            validate_receive_pack_status_message("receive-pack unpack error", message)?;
6018            Ok(format!("unpack {message}"))
6019        }
6020    }
6021}
6022
6023fn parse_receive_pack_command_status(payload: &[u8]) -> Result<ReceivePackCommandStatus> {
6024    let text = parse_protocol_v2_line_text("receive-pack command status", payload)?;
6025    if let Some(name) = text.strip_prefix("ok ") {
6026        validate_protocol_v2_token("receive-pack status ref name", name)?;
6027        return Ok(ReceivePackCommandStatus::Ok {
6028            name: name.to_string(),
6029        });
6030    }
6031    let Some(rest) = text.strip_prefix("ng ") else {
6032        return Err(GitError::InvalidFormat(format!(
6033            "unsupported receive-pack command status {text}"
6034        )));
6035    };
6036    let (name, message) = rest
6037        .split_once(' ')
6038        .ok_or_else(|| GitError::InvalidFormat("receive-pack ng status missing message".into()))?;
6039    validate_protocol_v2_token("receive-pack status ref name", name)?;
6040    validate_receive_pack_status_message("receive-pack ng status message", message)?;
6041    Ok(ReceivePackCommandStatus::Ng {
6042        name: name.to_string(),
6043        message: message.to_string(),
6044    })
6045}
6046
6047fn format_receive_pack_command_status(status: &ReceivePackCommandStatus) -> Result<String> {
6048    match status {
6049        ReceivePackCommandStatus::Ok { name } => {
6050            validate_protocol_v2_token("receive-pack status ref name", name)?;
6051            Ok(format!("ok {name}"))
6052        }
6053        ReceivePackCommandStatus::Ng { name, message } => {
6054            validate_protocol_v2_token("receive-pack status ref name", name)?;
6055            validate_receive_pack_status_message("receive-pack ng status message", message)?;
6056            Ok(format!("ng {name} {message}"))
6057        }
6058    }
6059}
6060
6061fn parse_receive_pack_command_status_v2(text: &str) -> Result<ReceivePackCommandStatusV2> {
6062    if let Some(name) = text.strip_prefix("ok ") {
6063        validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6064        return Ok(ReceivePackCommandStatusV2::Ok {
6065            name: name.to_string(),
6066            options: ReceivePackCommandStatusV2Options::default(),
6067        });
6068    }
6069    let Some(rest) = text.strip_prefix("ng ") else {
6070        return Err(GitError::InvalidFormat(format!(
6071            "unsupported receive-pack report-status-v2 line {text}"
6072        )));
6073    };
6074    let (name, message) = rest.split_once(' ').ok_or_else(|| {
6075        GitError::InvalidFormat("receive-pack status-v2 ng status missing message".into())
6076    })?;
6077    validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6078    validate_receive_pack_status_message("receive-pack status-v2 ng message", message)?;
6079    Ok(ReceivePackCommandStatusV2::Ng {
6080        name: name.to_string(),
6081        message: message.to_string(),
6082    })
6083}
6084
6085fn format_receive_pack_command_status_v2(status: &ReceivePackCommandStatusV2) -> Result<String> {
6086    match status {
6087        ReceivePackCommandStatusV2::Ok { name, .. } => {
6088            validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6089            Ok(format!("ok {name}"))
6090        }
6091        ReceivePackCommandStatusV2::Ng { name, message } => {
6092            validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6093            validate_receive_pack_status_message("receive-pack status-v2 ng message", message)?;
6094            Ok(format!("ng {name} {message}"))
6095        }
6096    }
6097}
6098
6099fn parse_receive_pack_report_status_v2_option(
6100    format: ObjectFormat,
6101    text: &str,
6102    options: &mut ReceivePackCommandStatusV2Options,
6103) -> Result<()> {
6104    if let Some(refname) = text.strip_prefix("option refname ") {
6105        if options.refname.is_some() {
6106            return Err(GitError::InvalidFormat(
6107                "receive-pack report-status-v2 has duplicate refname option".into(),
6108            ));
6109        }
6110        validate_protocol_v2_token("receive-pack status-v2 option refname", refname)?;
6111        options.refname = Some(refname.to_string());
6112    } else if let Some(old_oid) = text.strip_prefix("option old-oid ") {
6113        if options.old_oid.is_some() {
6114            return Err(GitError::InvalidFormat(
6115                "receive-pack report-status-v2 has duplicate old-oid option".into(),
6116            ));
6117        }
6118        validate_protocol_v2_token("receive-pack status-v2 option old-oid", old_oid)?;
6119        options.old_oid = Some(ObjectId::from_hex(format, old_oid)?);
6120    } else if let Some(new_oid) = text.strip_prefix("option new-oid ") {
6121        if options.new_oid.is_some() {
6122            return Err(GitError::InvalidFormat(
6123                "receive-pack report-status-v2 has duplicate new-oid option".into(),
6124            ));
6125        }
6126        validate_protocol_v2_token("receive-pack status-v2 option new-oid", new_oid)?;
6127        options.new_oid = Some(ObjectId::from_hex(format, new_oid)?);
6128    } else if text == "option forced-update" {
6129        if options.forced_update {
6130            return Err(GitError::InvalidFormat(
6131                "receive-pack report-status-v2 has duplicate forced-update option".into(),
6132            ));
6133        }
6134        options.forced_update = true;
6135    } else {
6136        return Err(GitError::InvalidFormat(format!(
6137            "unsupported receive-pack report-status-v2 option {text}"
6138        )));
6139    }
6140    Ok(())
6141}
6142
6143fn format_receive_pack_report_status_v2_options(
6144    options: &ReceivePackCommandStatusV2Options,
6145) -> Result<Vec<String>> {
6146    let mut out = Vec::new();
6147    if let Some(refname) = &options.refname {
6148        validate_protocol_v2_token("receive-pack status-v2 option refname", refname)?;
6149        out.push(format!("option refname {refname}"));
6150    }
6151    if let Some(old_oid) = &options.old_oid {
6152        out.push(format!("option old-oid {old_oid}"));
6153    }
6154    if let Some(new_oid) = &options.new_oid {
6155        out.push(format!("option new-oid {new_oid}"));
6156    }
6157    if options.forced_update {
6158        out.push("option forced-update".into());
6159    }
6160    Ok(out)
6161}
6162
6163fn validate_receive_pack_status_message(label: &str, message: &str) -> Result<()> {
6164    if message.is_empty() {
6165        return Err(GitError::InvalidFormat(format!("{label} is empty")));
6166    }
6167    if message
6168        .bytes()
6169        .any(|byte| matches!(byte, b'\n' | b'\r' | 0))
6170    {
6171        return Err(GitError::InvalidFormat(format!(
6172            "{label} contains a delimiter byte"
6173        )));
6174    }
6175    Ok(())
6176}
6177
6178fn validate_receive_pack_push_option(option: &[u8]) -> Result<()> {
6179    if option.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
6180        return Err(GitError::InvalidFormat(
6181            "receive-pack push-option contains a delimiter byte".into(),
6182        ));
6183    }
6184    Ok(())
6185}
6186
6187fn validate_protocol_error_message(message: &str) -> Result<()> {
6188    if message.is_empty() {
6189        return Err(GitError::InvalidFormat(
6190            "protocol error message is empty".into(),
6191        ));
6192    }
6193    if message
6194        .bytes()
6195        .any(|byte| matches!(byte, b'\n' | b'\r' | 0))
6196    {
6197        return Err(GitError::InvalidFormat(
6198            "protocol error message contains a delimiter byte".into(),
6199        ));
6200    }
6201    Ok(())
6202}
6203
6204fn parse_capability_token(token: &str) -> Result<Capability> {
6205    if token.is_empty() {
6206        return Err(GitError::InvalidFormat("empty capability token".into()));
6207    }
6208    let (name, value) = token
6209        .split_once('=')
6210        .map_or((token, None), |(name, value)| (name, Some(value)));
6211    validate_capability_field("capability name", name)?;
6212    if let Some(value) = value {
6213        validate_capability_field("capability value", value)?;
6214    }
6215    Ok(Capability {
6216        name: name.to_string(),
6217        value: value.map(str::to_string),
6218    })
6219}
6220
6221fn parse_protocol_v2_capability_line(payload: &[u8]) -> Result<Capability> {
6222    let payload = trim_trailing_lf(payload);
6223    if payload.is_empty() {
6224        return Err(GitError::InvalidFormat(
6225            "empty protocol v2 capability line".into(),
6226        ));
6227    }
6228    let text =
6229        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6230    let (name, value) = text
6231        .split_once('=')
6232        .map_or((text, None), |(name, value)| (name, Some(value)));
6233    validate_capability_name(name)?;
6234    if let Some(value) = value {
6235        validate_protocol_v2_capability_value(value)?;
6236    }
6237    Ok(Capability {
6238        name: name.to_string(),
6239        value: value.map(str::to_string),
6240    })
6241}
6242
6243fn parse_protocol_v2_command_line(payload: &[u8]) -> Result<String> {
6244    let payload = trim_trailing_lf(payload);
6245    let text =
6246        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6247    let Some(command) = text.strip_prefix("command=") else {
6248        return Err(GitError::InvalidFormat(
6249            "protocol v2 command request missing command prefix".into(),
6250        ));
6251    };
6252    validate_capability_name(command)?;
6253    Ok(command.to_string())
6254}
6255
6256fn parse_protocol_v2_ls_refs_line(
6257    format: ObjectFormat,
6258    payload: &[u8],
6259) -> Result<ProtocolV2LsRefsRecord> {
6260    let payload = trim_trailing_lf(payload);
6261    if payload.is_empty() {
6262        return Err(GitError::InvalidFormat(
6263            "ls-refs response line is empty".into(),
6264        ));
6265    }
6266    let text =
6267        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6268    let mut fields = text.split(' ');
6269    let first = fields
6270        .next()
6271        .ok_or_else(|| GitError::InvalidFormat("ls-refs response line is empty".into()))?;
6272    if first == "unborn" {
6273        let name = fields
6274            .next()
6275            .ok_or_else(|| GitError::InvalidFormat("ls-refs unborn line is missing name".into()))?;
6276        validate_protocol_v2_token("ls-refs ref name", name)?;
6277        let (symref_target, attributes) = parse_protocol_v2_ls_refs_attributes(format, fields)?;
6278        return Ok(ProtocolV2LsRefsRecord::Unborn {
6279            name: name.to_string(),
6280            symref_target,
6281            attributes,
6282        });
6283    }
6284
6285    let oid = ObjectId::from_hex(format, first)?;
6286    let name = fields
6287        .next()
6288        .ok_or_else(|| GitError::InvalidFormat("ls-refs ref line is missing name".into()))?;
6289    validate_protocol_v2_token("ls-refs ref name", name)?;
6290    let (peeled, symref_target, attributes) =
6291        parse_protocol_v2_ls_refs_ref_attributes(format, fields)?;
6292    Ok(ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
6293        oid,
6294        name: name.to_string(),
6295        peeled,
6296        symref_target,
6297        attributes,
6298    }))
6299}
6300
6301fn parse_protocol_v2_ls_refs_ref_attributes<'a>(
6302    format: ObjectFormat,
6303    fields: impl Iterator<Item = &'a str>,
6304) -> Result<(Option<ObjectId>, Option<String>, Vec<String>)> {
6305    let mut peeled = None;
6306    let (symref_target, attributes) =
6307        parse_protocol_v2_ls_refs_attributes_with(format, fields, |attr| {
6308            if let Some(value) = attr.strip_prefix("peeled:") {
6309                if peeled.is_some() {
6310                    return Err(GitError::InvalidFormat(
6311                        "ls-refs response has duplicate peeled attribute".into(),
6312                    ));
6313                }
6314                peeled = Some(ObjectId::from_hex(format, value)?);
6315                return Ok(true);
6316            }
6317            Ok(false)
6318        })?;
6319    Ok((peeled, symref_target, attributes))
6320}
6321
6322fn parse_protocol_v2_ls_refs_attributes<'a>(
6323    format: ObjectFormat,
6324    fields: impl Iterator<Item = &'a str>,
6325) -> Result<(Option<String>, Vec<String>)> {
6326    parse_protocol_v2_ls_refs_attributes_with(format, fields, |attr| {
6327        if attr.starts_with("peeled:") {
6328            return Err(GitError::InvalidFormat(
6329                "ls-refs unborn line has peeled attribute".into(),
6330            ));
6331        }
6332        Ok(false)
6333    })
6334}
6335
6336fn parse_protocol_v2_ls_refs_attributes_with<'a, F>(
6337    _format: ObjectFormat,
6338    fields: impl Iterator<Item = &'a str>,
6339    mut handle_known: F,
6340) -> Result<(Option<String>, Vec<String>)>
6341where
6342    F: FnMut(&str) -> Result<bool>,
6343{
6344    let mut symref_target = None;
6345    let mut attributes = Vec::new();
6346    for attr in fields {
6347        validate_protocol_v2_token("ls-refs attribute", attr)?;
6348        if let Some(value) = attr.strip_prefix("symref-target:") {
6349            if symref_target.is_some() {
6350                return Err(GitError::InvalidFormat(
6351                    "ls-refs response has duplicate symref-target attribute".into(),
6352                ));
6353            }
6354            validate_protocol_v2_token("ls-refs symref-target", value)?;
6355            symref_target = Some(value.to_string());
6356        } else if !handle_known(attr)? {
6357            attributes.push(attr.to_string());
6358        }
6359    }
6360    Ok((symref_target, attributes))
6361}
6362
6363fn format_protocol_v2_ls_refs_record(record: &ProtocolV2LsRefsRecord) -> Result<String> {
6364    let mut out = String::new();
6365    match record {
6366        ProtocolV2LsRefsRecord::Ref(reference) => {
6367            validate_protocol_v2_token("ls-refs ref name", &reference.name)?;
6368            out.push_str(&reference.oid.to_string());
6369            out.push(' ');
6370            out.push_str(&reference.name);
6371            if let Some(peeled) = &reference.peeled {
6372                if peeled.format() != reference.oid.format() {
6373                    return Err(GitError::InvalidObjectId(
6374                        "ls-refs peeled object format does not match ref object format".into(),
6375                    ));
6376                }
6377                out.push(' ');
6378                out.push_str("peeled:");
6379                out.push_str(&peeled.to_string());
6380            }
6381            if let Some(target) = &reference.symref_target {
6382                validate_protocol_v2_token("ls-refs symref-target", target)?;
6383                out.push(' ');
6384                out.push_str("symref-target:");
6385                out.push_str(target);
6386            }
6387            append_protocol_v2_ls_refs_attributes(&mut out, &reference.attributes)?;
6388        }
6389        ProtocolV2LsRefsRecord::Unborn {
6390            name,
6391            symref_target,
6392            attributes,
6393        } => {
6394            validate_protocol_v2_token("ls-refs ref name", name)?;
6395            out.push_str("unborn ");
6396            out.push_str(name);
6397            if let Some(target) = symref_target {
6398                validate_protocol_v2_token("ls-refs symref-target", target)?;
6399                out.push(' ');
6400                out.push_str("symref-target:");
6401                out.push_str(target);
6402            }
6403            append_protocol_v2_ls_refs_attributes(&mut out, attributes)?;
6404        }
6405    }
6406    Ok(out)
6407}
6408
6409fn append_protocol_v2_ls_refs_attributes(out: &mut String, attributes: &[String]) -> Result<()> {
6410    for attr in attributes {
6411        validate_protocol_v2_token("ls-refs attribute", attr)?;
6412        if attr.starts_with("peeled:") || attr.starts_with("symref-target:") {
6413            return Err(GitError::InvalidFormat(
6414                "ls-refs generic attributes must not duplicate known attributes".into(),
6415            ));
6416        }
6417        out.push(' ');
6418        out.push_str(attr);
6419    }
6420    Ok(())
6421}
6422
6423fn parse_fetch_section_header(payload: &[u8]) -> Result<String> {
6424    let name = parse_protocol_v2_line_text("fetch response section", payload)?;
6425    validate_capability_name(name)?;
6426    Ok(name.to_string())
6427}
6428
6429fn flush_terminates_protocol_v2_response(frames: &[PktLineFrame], idx: usize) -> bool {
6430    idx + 1 == frames.len()
6431        || (idx + 2 == frames.len() && matches!(frames[idx + 1], PktLineFrame::ResponseEnd))
6432}
6433
6434fn parse_fetch_section(
6435    format: ObjectFormat,
6436    name: String,
6437    lines: Vec<Vec<u8>>,
6438) -> Result<ProtocolV2FetchResponseSection> {
6439    match name.as_str() {
6440        "acknowledgments" => lines
6441            .iter()
6442            .map(|line| parse_fetch_acknowledgment(format, line))
6443            .collect::<Result<Vec<_>>>()
6444            .map(ProtocolV2FetchResponseSection::Acknowledgments),
6445        "shallow-info" => lines
6446            .iter()
6447            .map(|line| parse_fetch_shallow_info(format, line))
6448            .collect::<Result<Vec<_>>>()
6449            .map(ProtocolV2FetchResponseSection::ShallowInfo),
6450        "wanted-refs" => lines
6451            .iter()
6452            .map(|line| parse_fetch_wanted_ref(format, line))
6453            .collect::<Result<Vec<_>>>()
6454            .map(ProtocolV2FetchResponseSection::WantedRefs),
6455        "packfile-uris" => lines
6456            .iter()
6457            .map(|line| parse_fetch_packfile_uri(format, line))
6458            .collect::<Result<Vec<_>>>()
6459            .map(ProtocolV2FetchResponseSection::PackfileUris),
6460        "packfile" => Ok(ProtocolV2FetchResponseSection::Packfile(lines)),
6461        _ => Ok(ProtocolV2FetchResponseSection::Unknown { name, lines }),
6462    }
6463}
6464
6465fn parse_fetch_acknowledgment(
6466    format: ObjectFormat,
6467    line: &[u8],
6468) -> Result<ProtocolV2FetchAcknowledgment> {
6469    let text = parse_protocol_v2_line_text("fetch acknowledgment", line)?;
6470    match text {
6471        "NAK" => Ok(ProtocolV2FetchAcknowledgment::Nak),
6472        "ready" => Ok(ProtocolV2FetchAcknowledgment::Ready),
6473        value if value.starts_with("ACK ") => Ok(ProtocolV2FetchAcknowledgment::Ack(
6474            parse_oid_argument(format, "fetch ACK", value, "ACK ")?,
6475        )),
6476        other => Err(GitError::InvalidFormat(format!(
6477            "unsupported fetch acknowledgment {other}"
6478        ))),
6479    }
6480}
6481
6482fn parse_fetch_shallow_info(
6483    format: ObjectFormat,
6484    line: &[u8],
6485) -> Result<ProtocolV2FetchShallowInfo> {
6486    let text = parse_protocol_v2_line_text("fetch shallow-info", line)?;
6487    if text.starts_with("shallow ") {
6488        return Ok(ProtocolV2FetchShallowInfo::Shallow(parse_oid_argument(
6489            format,
6490            "fetch shallow",
6491            text,
6492            "shallow ",
6493        )?));
6494    }
6495    if text.starts_with("unshallow ") {
6496        return Ok(ProtocolV2FetchShallowInfo::Unshallow(parse_oid_argument(
6497            format,
6498            "fetch unshallow",
6499            text,
6500            "unshallow ",
6501        )?));
6502    }
6503    Err(GitError::InvalidFormat(format!(
6504        "unsupported fetch shallow-info {text}"
6505    )))
6506}
6507
6508fn parse_fetch_wanted_ref(format: ObjectFormat, line: &[u8]) -> Result<ProtocolV2FetchWantedRef> {
6509    let text = parse_protocol_v2_line_text("fetch wanted-ref", line)?;
6510    let (oid, name) = text
6511        .split_once(' ')
6512        .ok_or_else(|| GitError::InvalidFormat("fetch wanted-ref is missing name".into()))?;
6513    validate_protocol_v2_token("fetch wanted-ref name", name)?;
6514    Ok(ProtocolV2FetchWantedRef {
6515        oid: ObjectId::from_hex(format, oid)?,
6516        name: name.to_string(),
6517    })
6518}
6519
6520fn parse_fetch_packfile_uri(
6521    format: ObjectFormat,
6522    line: &[u8],
6523) -> Result<ProtocolV2FetchPackfileUri> {
6524    let text = parse_protocol_v2_line_text("fetch packfile-uri", line)?;
6525    let (pack_hash, uri) = text
6526        .split_once(' ')
6527        .ok_or_else(|| GitError::InvalidFormat("fetch packfile-uri is missing uri".into()))?;
6528    validate_protocol_v2_token("fetch packfile-uri hash", pack_hash)?;
6529    validate_protocol_v2_token("fetch packfile-uri", uri)?;
6530    Ok(ProtocolV2FetchPackfileUri {
6531        pack_hash: ObjectId::from_hex(format, pack_hash)?,
6532        uri: uri.to_string(),
6533    })
6534}
6535
6536fn protocol_v2_fetch_section_name(section: &ProtocolV2FetchResponseSection) -> &str {
6537    match section {
6538        ProtocolV2FetchResponseSection::Acknowledgments(_) => "acknowledgments",
6539        ProtocolV2FetchResponseSection::ShallowInfo(_) => "shallow-info",
6540        ProtocolV2FetchResponseSection::WantedRefs(_) => "wanted-refs",
6541        ProtocolV2FetchResponseSection::PackfileUris(_) => "packfile-uris",
6542        ProtocolV2FetchResponseSection::Packfile(_) => "packfile",
6543        ProtocolV2FetchResponseSection::Unknown { name, .. } => name,
6544    }
6545}
6546
6547fn format_protocol_v2_fetch_section_lines(
6548    section: &ProtocolV2FetchResponseSection,
6549) -> Result<Vec<Vec<u8>>> {
6550    match section {
6551        ProtocolV2FetchResponseSection::Acknowledgments(acks) => acks
6552            .iter()
6553            .map(|ack| match ack {
6554                ProtocolV2FetchAcknowledgment::Nak => Ok(line_from_str("NAK")),
6555                ProtocolV2FetchAcknowledgment::Ack(oid) => Ok(line_from_str(&format!("ACK {oid}"))),
6556                ProtocolV2FetchAcknowledgment::Ready => Ok(line_from_str("ready")),
6557            })
6558            .collect(),
6559        ProtocolV2FetchResponseSection::ShallowInfo(entries) => entries
6560            .iter()
6561            .map(|entry| match entry {
6562                ProtocolV2FetchShallowInfo::Shallow(oid) => {
6563                    Ok(line_from_str(&format!("shallow {oid}")))
6564                }
6565                ProtocolV2FetchShallowInfo::Unshallow(oid) => {
6566                    Ok(line_from_str(&format!("unshallow {oid}")))
6567                }
6568            })
6569            .collect(),
6570        ProtocolV2FetchResponseSection::WantedRefs(refs) => refs
6571            .iter()
6572            .map(|wanted| {
6573                validate_protocol_v2_token("fetch wanted-ref name", &wanted.name)?;
6574                Ok(line_from_str(&format!("{} {}", wanted.oid, wanted.name)))
6575            })
6576            .collect(),
6577        ProtocolV2FetchResponseSection::PackfileUris(uris) => uris
6578            .iter()
6579            .map(|packfile_uri| {
6580                validate_protocol_v2_token("fetch packfile-uri", &packfile_uri.uri)?;
6581                Ok(line_from_str(&format!(
6582                    "{} {}",
6583                    packfile_uri.pack_hash, packfile_uri.uri
6584                )))
6585            })
6586            .collect(),
6587        ProtocolV2FetchResponseSection::Packfile(lines) => Ok(lines.clone()),
6588        ProtocolV2FetchResponseSection::Unknown { name, lines } => {
6589            validate_capability_name(name)?;
6590            for line in lines {
6591                validate_protocol_v2_line("fetch unknown section line", line)?;
6592            }
6593            Ok(lines.clone())
6594        }
6595    }
6596}
6597
6598fn parse_protocol_v2_object_info_record(
6599    format: ObjectFormat,
6600    line: &[u8],
6601) -> Result<ProtocolV2ObjectInfoRecord> {
6602    let text = parse_protocol_v2_line_text("object-info record", line)?;
6603    let mut fields = text.split(' ');
6604    let oid = fields
6605        .next()
6606        .ok_or_else(|| GitError::InvalidFormat("object-info record is missing oid".into()))?;
6607    let size = fields
6608        .next()
6609        .ok_or_else(|| GitError::InvalidFormat("object-info record is missing size".into()))?;
6610    if fields.next().is_some() {
6611        return Err(GitError::InvalidFormat(
6612            "object-info record has too many fields".into(),
6613        ));
6614    }
6615    validate_protocol_v2_token("object-info oid", oid)?;
6616    validate_protocol_v2_token("object-info size", size)?;
6617    let size = size
6618        .parse::<u64>()
6619        .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6620    Ok(ProtocolV2ObjectInfoRecord {
6621        oid: ObjectId::from_hex(format, oid)?,
6622        size,
6623    })
6624}
6625
6626fn parse_dumb_http_info_ref_record(format: ObjectFormat, line: &[u8]) -> Result<DumbHttpRefRecord> {
6627    validate_dumb_http_info_ref_line(line)?;
6628    let line = trim_trailing_lf(line);
6629    let tab = line
6630        .iter()
6631        .position(|byte| *byte == b'\t')
6632        .ok_or_else(|| GitError::InvalidFormat("dumb HTTP ref record is missing name".into()))?;
6633    let (oid, name) = (&line[..tab], &line[tab + 1..]);
6634    let oid = std::str::from_utf8(oid).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6635    validate_protocol_v2_token("dumb HTTP ref oid", oid)?;
6636    let name = std::str::from_utf8(name).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6637    let (name, peeled) = name
6638        .strip_suffix("^{}")
6639        .map_or((name, false), |name| (name, true));
6640    validate_dumb_http_ref_name(name)?;
6641    Ok(DumbHttpRefRecord {
6642        oid: ObjectId::from_hex(format, oid)?,
6643        name: name.to_string(),
6644        peeled,
6645    })
6646}
6647
6648fn parse_dumb_http_alternate(line: &[u8]) -> Result<String> {
6649    validate_dumb_http_alternate_line(line)?;
6650    let line = trim_trailing_lf(line);
6651    let alternate =
6652        std::str::from_utf8(line).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6653    validate_dumb_http_alternate(alternate)?;
6654    Ok(alternate.to_string())
6655}
6656
6657fn parse_dumb_http_pack_record(format: ObjectFormat, line: &[u8]) -> Result<DumbHttpPackRecord> {
6658    validate_dumb_http_info_ref_line(line)?;
6659    let line = parse_protocol_v2_line_text("dumb HTTP pack record", line)?;
6660    let pack_name = line
6661        .strip_prefix("P ")
6662        .ok_or_else(|| GitError::InvalidFormat("dumb HTTP pack record must start with P".into()))?;
6663    let hash = pack_name
6664        .strip_prefix("pack-")
6665        .and_then(|value| value.strip_suffix(".pack"))
6666        .ok_or_else(|| GitError::InvalidFormat("invalid dumb HTTP pack name".into()))?;
6667    validate_protocol_v2_token("dumb HTTP pack hash", hash)?;
6668    Ok(DumbHttpPackRecord {
6669        hash: ObjectId::from_hex(format, hash)?,
6670    })
6671}
6672
6673fn encode_protocol_v2_capability(capability: &Capability) -> Result<Vec<u8>> {
6674    validate_capability_name(&capability.name)?;
6675    let mut out = capability.name.as_bytes().to_vec();
6676    if let Some(value) = &capability.value {
6677        validate_protocol_v2_capability_value(value)?;
6678        out.push(b'=');
6679        out.extend_from_slice(value.as_bytes());
6680    }
6681    Ok(out)
6682}
6683
6684fn validate_capability_field(label: &str, value: &str) -> Result<()> {
6685    if value.is_empty() {
6686        return Err(GitError::InvalidFormat(format!("{label} is empty")));
6687    }
6688    if value
6689        .bytes()
6690        .any(|byte| matches!(byte, b' ' | b'\n' | b'\r' | b'\t' | 0))
6691    {
6692        return Err(GitError::InvalidFormat(format!(
6693            "{label} contains a delimiter byte"
6694        )));
6695    }
6696    Ok(())
6697}
6698
6699fn validate_capability_name(value: &str) -> Result<()> {
6700    validate_capability_field("capability name", value)?;
6701    if value.bytes().any(|byte| byte == b'=') {
6702        return Err(GitError::InvalidFormat(
6703            "capability name contains a delimiter byte".into(),
6704        ));
6705    }
6706    Ok(())
6707}
6708
6709fn validate_protocol_v2_capability_value(value: &str) -> Result<()> {
6710    if value.is_empty() {
6711        return Err(GitError::InvalidFormat(
6712            "protocol v2 capability value is empty".into(),
6713        ));
6714    }
6715    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6716        return Err(GitError::InvalidFormat(
6717            "protocol v2 capability value contains a delimiter byte".into(),
6718        ));
6719    }
6720    Ok(())
6721}
6722
6723fn validate_protocol_v2_argument(value: &[u8]) -> Result<()> {
6724    if value.is_empty() {
6725        return Err(GitError::InvalidFormat(
6726            "protocol v2 command argument is empty".into(),
6727        ));
6728    }
6729    if value.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
6730        return Err(GitError::InvalidFormat(
6731            "protocol v2 command argument contains a delimiter byte".into(),
6732        ));
6733    }
6734    Ok(())
6735}
6736
6737fn validate_upload_archive_argument(value: &str) -> Result<()> {
6738    if value.is_empty() {
6739        return Err(GitError::InvalidFormat(
6740            "upload-archive argument is empty".into(),
6741        ));
6742    }
6743    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6744        return Err(GitError::InvalidFormat(
6745            "upload-archive argument contains a delimiter byte".into(),
6746        ));
6747    }
6748    Ok(())
6749}
6750
6751fn validate_upload_archive_status_message(value: &str) -> Result<()> {
6752    if value.is_empty() {
6753        return Err(GitError::InvalidFormat(
6754            "upload-archive status message is empty".into(),
6755        ));
6756    }
6757    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6758        return Err(GitError::InvalidFormat(
6759            "upload-archive status message contains a delimiter byte".into(),
6760        ));
6761    }
6762    Ok(())
6763}
6764
6765fn non_empty(value: &str) -> Option<&str> {
6766    (!value.is_empty()).then_some(value)
6767}
6768
6769fn validate_refspec_value(value: &str) -> Result<()> {
6770    if value.is_empty() {
6771        return Err(GitError::InvalidFormat("refspec is empty".into()));
6772    }
6773    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6774        return Err(GitError::InvalidFormat(
6775            "refspec contains a delimiter byte".into(),
6776        ));
6777    }
6778    Ok(())
6779}
6780
6781fn validate_refspec_endpoint(label: &str, value: &str) -> Result<()> {
6782    if value.is_empty() {
6783        return Err(GitError::InvalidFormat(format!("{label} is empty")));
6784    }
6785    if value
6786        .bytes()
6787        .any(|byte| matches!(byte, b':' | b' ' | b'\t' | b'\n' | b'\r' | 0))
6788    {
6789        return Err(GitError::InvalidFormat(format!(
6790            "{label} contains a delimiter byte"
6791        )));
6792    }
6793    Ok(())
6794}
6795
6796fn count_refspec_wildcards(value: &str) -> usize {
6797    value.bytes().filter(|byte| *byte == b'*').count()
6798}
6799
6800fn validate_refspec_shape(refspec: &RefSpec) -> Result<()> {
6801    if refspec.force && refspec.negative {
6802        return Err(GitError::InvalidFormat(
6803            "negative refspec must not be forced".into(),
6804        ));
6805    }
6806    if refspec.negative && refspec.dst.is_some() {
6807        return Err(GitError::InvalidFormat(
6808            "negative refspec must not have a destination".into(),
6809        ));
6810    }
6811    if refspec.negative && refspec.src.is_none() {
6812        return Err(GitError::InvalidFormat(
6813            "negative refspec is missing a source".into(),
6814        ));
6815    }
6816    if refspec.src.is_none() && refspec.dst.is_none() && refspec.negative {
6817        return Err(GitError::InvalidFormat(
6818            "refspec must include a source or destination".into(),
6819        ));
6820    }
6821    if let Some(src) = &refspec.src {
6822        validate_refspec_endpoint("refspec source", src)?;
6823    }
6824    if let Some(dst) = &refspec.dst {
6825        validate_refspec_endpoint("refspec destination", dst)?;
6826    }
6827    let src_pattern_count = refspec
6828        .src
6829        .as_deref()
6830        .map(count_refspec_wildcards)
6831        .unwrap_or(0);
6832    let dst_pattern_count = refspec
6833        .dst
6834        .as_deref()
6835        .map(count_refspec_wildcards)
6836        .unwrap_or(0);
6837    if src_pattern_count > 1 || dst_pattern_count > 1 {
6838        return Err(GitError::InvalidFormat(
6839            "refspec endpoint has too many wildcards".into(),
6840        ));
6841    }
6842    if refspec.dst.is_some() && (src_pattern_count == 1) != (dst_pattern_count == 1) {
6843        return Err(GitError::InvalidFormat(
6844            "refspec wildcard must appear in both source and destination".into(),
6845        ));
6846    }
6847    if refspec.pattern != (src_pattern_count == 1 || dst_pattern_count == 1) {
6848        return Err(GitError::InvalidFormat(
6849            "refspec pattern flag does not match endpoints".into(),
6850        ));
6851    }
6852    Ok(())
6853}
6854
6855fn parse_fetch_head_record(format: ObjectFormat, line: &[u8]) -> Result<FetchHeadRecord> {
6856    validate_fetch_head_line(line)?;
6857    let line = trim_trailing_lf(line);
6858    let mut fields = line.splitn(3, |byte| *byte == b'\t');
6859    let oid = fields
6860        .next()
6861        .ok_or_else(|| GitError::InvalidFormat("FETCH_HEAD record is missing oid".into()))?;
6862    let merge_marker = fields.next().ok_or_else(|| {
6863        GitError::InvalidFormat("FETCH_HEAD record is missing merge marker".into())
6864    })?;
6865    let description = fields.next().ok_or_else(|| {
6866        GitError::InvalidFormat("FETCH_HEAD record is missing description".into())
6867    })?;
6868    let oid = std::str::from_utf8(oid).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6869    validate_protocol_v2_token("FETCH_HEAD oid", oid)?;
6870    let not_for_merge = match merge_marker {
6871        b"" => false,
6872        b"not-for-merge" => true,
6873        _ => {
6874            return Err(GitError::InvalidFormat(
6875                "FETCH_HEAD record has invalid merge marker".into(),
6876            ));
6877        }
6878    };
6879    let description =
6880        std::str::from_utf8(description).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6881    validate_fetch_head_description_field(description)?;
6882    Ok(FetchHeadRecord {
6883        oid: ObjectId::from_hex(format, oid)?,
6884        not_for_merge,
6885        description: description.to_string(),
6886    })
6887}
6888
6889fn validate_fetch_head_line(value: &[u8]) -> Result<()> {
6890    if value.is_empty() {
6891        return Err(GitError::InvalidFormat("FETCH_HEAD record is empty".into()));
6892    }
6893    if !value.ends_with(b"\n") {
6894        return Err(GitError::InvalidFormat(
6895            "FETCH_HEAD record missing LF".into(),
6896        ));
6897    }
6898    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
6899        return Err(GitError::InvalidFormat(
6900            "FETCH_HEAD record contains a delimiter byte".into(),
6901        ));
6902    }
6903    Ok(())
6904}
6905
6906fn validate_fetch_head_description_field(value: &str) -> Result<()> {
6907    if value.is_empty() {
6908        return Err(GitError::InvalidFormat(
6909            "FETCH_HEAD description is empty".into(),
6910        ));
6911    }
6912    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6913        return Err(GitError::InvalidFormat(
6914            "FETCH_HEAD description contains a delimiter byte".into(),
6915        ));
6916    }
6917    Ok(())
6918}
6919
6920fn refspec_is_excluded(negative: &[&RefSpec], source: &str) -> Result<bool> {
6921    for refspec in negative {
6922        if refspec_matches_source(refspec, source)? {
6923            return Ok(true);
6924        }
6925    }
6926    Ok(false)
6927}
6928
6929fn zero_object_id(format: ObjectFormat) -> Result<ObjectId> {
6930    Ok(ObjectId::null(format))
6931}
6932
6933fn local_ref<'a>(refs: &'a [PushSourceRef], name: &str) -> Option<&'a PushSourceRef> {
6934    refs.iter().find(|reference| reference.name == name)
6935}
6936
6937fn remote_ref<'a>(refs: &'a [RefAdvertisement], name: &str) -> Option<&'a RefAdvertisement> {
6938    refs.iter().find(|reference| reference.name == name)
6939}
6940
6941fn validate_push_source_ref(format: ObjectFormat, reference: &PushSourceRef) -> Result<()> {
6942    if reference.oid.format() != format {
6943        return Err(GitError::InvalidObjectId(
6944            "push source ref object format does not match repository".into(),
6945        ));
6946    }
6947    validate_refspec_endpoint("push source ref name", &reference.name)
6948}
6949
6950fn require_receive_pack_feature(advertised: bool, name: &str) -> Result<()> {
6951    if advertised {
6952        Ok(())
6953    } else {
6954        Err(GitError::InvalidFormat(format!(
6955            "receive-pack feature {name} was not advertised"
6956        )))
6957    }
6958}
6959
6960fn validate_smart_http_service(service: GitService) -> Result<()> {
6961    match service {
6962        GitService::UploadPack | GitService::ReceivePack => Ok(()),
6963        GitService::UploadArchive => Err(GitError::InvalidFormat(
6964            "smart HTTP only supports upload-pack and receive-pack services".into(),
6965        )),
6966    }
6967}
6968
6969fn normalize_http_repository_path(path: &str) -> Result<String> {
6970    if path.is_empty() {
6971        return Err(GitError::InvalidFormat(
6972            "smart HTTP repository path is empty".into(),
6973        ));
6974    }
6975    if !path.starts_with('/') {
6976        return Err(GitError::InvalidFormat(
6977            "smart HTTP repository path must start with /".into(),
6978        ));
6979    }
6980    if path
6981        .bytes()
6982        .any(|byte| matches!(byte, b'\n' | b'\r' | 0 | b'?' | b'#'))
6983    {
6984        return Err(GitError::InvalidFormat(
6985            "smart HTTP repository path contains a delimiter byte".into(),
6986        ));
6987    }
6988    let normalized = path.trim_end_matches('/');
6989    Ok(if normalized.is_empty() {
6990        "/".into()
6991    } else {
6992        normalized.to_string()
6993    })
6994}
6995
6996fn dumb_http_pack_resource_path(
6997    repository_path: &str,
6998    hash: &ObjectId,
6999    suffix: &str,
7000) -> Result<String> {
7001    let repository_path = normalize_http_repository_path(repository_path)?;
7002    Ok(format!(
7003        "{repository_path}/objects/pack/pack-{hash}.{suffix}"
7004    ))
7005}
7006
7007fn parse_smart_http_content_type(value: &str, suffix: &str) -> Result<GitService> {
7008    let value = value.trim();
7009    if value.is_empty() {
7010        return Err(GitError::InvalidFormat(
7011            "smart HTTP content type is empty".into(),
7012        ));
7013    }
7014    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
7015        return Err(GitError::InvalidFormat(
7016            "smart HTTP content type contains a delimiter byte".into(),
7017        ));
7018    }
7019    let value = value.to_ascii_lowercase();
7020    let service = value
7021        .strip_prefix("application/x-")
7022        .and_then(|value| value.strip_suffix(suffix))
7023        .ok_or_else(|| GitError::InvalidFormat("invalid smart HTTP content type".into()))?;
7024    let service = parse_git_service(service)?;
7025    validate_smart_http_service(service)?;
7026    Ok(service)
7027}
7028
7029fn validate_dumb_http_info_ref_line(value: &[u8]) -> Result<()> {
7030    if value.is_empty() {
7031        return Err(GitError::InvalidFormat(
7032            "dumb HTTP ref record is empty".into(),
7033        ));
7034    }
7035    if !value.ends_with(b"\n") {
7036        return Err(GitError::InvalidFormat(
7037            "dumb HTTP ref record missing LF".into(),
7038        ));
7039    }
7040    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7041        return Err(GitError::InvalidFormat(
7042            "dumb HTTP ref record contains a delimiter byte".into(),
7043        ));
7044    }
7045    Ok(())
7046}
7047
7048fn validate_dumb_http_ref_name(value: &str) -> Result<()> {
7049    validate_protocol_v2_token("dumb HTTP ref name", value)?;
7050    if value.ends_with("^{}") {
7051        return Err(GitError::InvalidFormat(
7052            "dumb HTTP ref name must not include peeled suffix".into(),
7053        ));
7054    }
7055    Ok(())
7056}
7057
7058fn validate_dumb_http_alternate_line(value: &[u8]) -> Result<()> {
7059    if value.is_empty() {
7060        return Err(GitError::InvalidFormat(
7061            "dumb HTTP alternate is empty".into(),
7062        ));
7063    }
7064    if !value.ends_with(b"\n") {
7065        return Err(GitError::InvalidFormat(
7066            "dumb HTTP alternate missing LF".into(),
7067        ));
7068    }
7069    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7070        return Err(GitError::InvalidFormat(
7071            "dumb HTTP alternate contains a delimiter byte".into(),
7072        ));
7073    }
7074    Ok(())
7075}
7076
7077fn validate_dumb_http_alternate(value: &str) -> Result<()> {
7078    if value.is_empty() {
7079        return Err(GitError::InvalidFormat(
7080            "dumb HTTP alternate is empty".into(),
7081        ));
7082    }
7083    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
7084        return Err(GitError::InvalidFormat(
7085            "dumb HTTP alternate contains a delimiter byte".into(),
7086        ));
7087    }
7088    Ok(())
7089}
7090
7091fn validate_protocol_v2_token(label: &str, value: &str) -> Result<()> {
7092    if value.is_empty() {
7093        return Err(GitError::InvalidFormat(format!("{label} is empty")));
7094    }
7095    if value
7096        .bytes()
7097        .any(|byte| matches!(byte, b' ' | b'\n' | b'\r' | 0))
7098    {
7099        return Err(GitError::InvalidFormat(format!(
7100            "{label} contains a delimiter byte"
7101        )));
7102    }
7103    Ok(())
7104}
7105
7106fn validate_protocol_v2_line(label: &str, value: &[u8]) -> Result<()> {
7107    if value.is_empty() {
7108        return Err(GitError::InvalidFormat(format!("{label} is empty")));
7109    }
7110    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7111        return Err(GitError::InvalidFormat(format!(
7112            "{label} contains a delimiter byte"
7113        )));
7114    }
7115    Ok(())
7116}
7117
7118fn parse_protocol_v2_line_text<'a>(label: &str, value: &'a [u8]) -> Result<&'a str> {
7119    validate_protocol_v2_line(label, value)?;
7120    let value = trim_trailing_lf(value);
7121    if value.is_empty() {
7122        return Err(GitError::InvalidFormat(format!("{label} is empty")));
7123    }
7124    if value.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
7125        return Err(GitError::InvalidFormat(format!(
7126            "{label} contains a delimiter byte"
7127        )));
7128    }
7129    std::str::from_utf8(value).map_err(|err| GitError::InvalidFormat(err.to_string()))
7130}
7131
7132fn parse_oid_argument(
7133    format: ObjectFormat,
7134    label: &str,
7135    value: &str,
7136    prefix: &str,
7137) -> Result<ObjectId> {
7138    let oid = value
7139        .strip_prefix(prefix)
7140        .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
7141    validate_protocol_v2_token(label, oid)?;
7142    ObjectId::from_hex(format, oid)
7143}
7144
7145fn parse_u32_argument(label: &str, value: &str, prefix: &str) -> Result<u32> {
7146    let number = value
7147        .strip_prefix(prefix)
7148        .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
7149    validate_protocol_v2_token(label, number)?;
7150    let parsed = number
7151        .parse::<u32>()
7152        .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
7153    if parsed == 0 {
7154        return Err(GitError::InvalidFormat(format!("{label} must be positive")));
7155    }
7156    Ok(parsed)
7157}
7158
7159fn parse_u64_argument(label: &str, value: &str, prefix: &str) -> Result<u64> {
7160    let number = value
7161        .strip_prefix(prefix)
7162        .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
7163    validate_protocol_v2_token(label, number)?;
7164    number
7165        .parse::<u64>()
7166        .map_err(|err| GitError::InvalidFormat(err.to_string()))
7167}
7168
7169fn line(mut payload: Vec<u8>) -> Vec<u8> {
7170    payload.push(b'\n');
7171    payload
7172}
7173
7174fn line_from_str(payload: &str) -> Vec<u8> {
7175    line(payload.as_bytes().to_vec())
7176}
7177
7178fn trim_trailing_lf(input: &[u8]) -> &[u8] {
7179    input.strip_suffix(b"\n").unwrap_or(input)
7180}
7181
7182#[cfg(test)]
7183mod tests {
7184    use super::*;
7185
7186    #[test]
7187    fn pkt_line_frame_encodes_data_and_control_frames() {
7188        assert_eq!(PktLine(b"hello\n".to_vec()).encode(), b"000ahello\n");
7189        assert_eq!(
7190            PktLineFrame::data(b"hello\n".to_vec())
7191                .expect("test operation should succeed")
7192                .encode(),
7193            b"000ahello\n"
7194        );
7195        assert_eq!(PktLineFrame::Flush.encode(), b"0000");
7196        assert_eq!(PktLineFrame::Delimiter.encode(), b"0001");
7197        assert_eq!(PktLineFrame::ResponseEnd.encode(), b"0002");
7198        assert_eq!(
7199            PktLineFrame::data(b"hello\n".to_vec())
7200                .expect("test operation should succeed")
7201                .try_encode()
7202                .expect("test operation should succeed"),
7203            b"000ahello\n"
7204        );
7205    }
7206
7207    #[test]
7208    fn pkt_line_frame_parses_data_and_control_frames() {
7209        assert_eq!(
7210            PktLineFrame::parse(b"000ahello\n").expect("test operation should succeed"),
7211            (PktLineFrame::Data(b"hello\n".to_vec()), 10)
7212        );
7213        assert_eq!(
7214            PktLineFrame::parse(b"0000").expect("test operation should succeed"),
7215            (PktLineFrame::Flush, 4)
7216        );
7217        assert_eq!(
7218            PktLineFrame::parse(b"0001").expect("test operation should succeed"),
7219            (PktLineFrame::Delimiter, 4)
7220        );
7221        assert_eq!(
7222            PktLineFrame::parse(b"0002").expect("test operation should succeed"),
7223            (PktLineFrame::ResponseEnd, 4)
7224        );
7225    }
7226
7227    #[test]
7228    fn pkt_line_stream_parses_multiple_frames() {
7229        let frames = parse_pkt_line_stream(b"000eversion 2\n00010009done\n0000")
7230            .expect("test operation should succeed");
7231        assert_eq!(
7232            frames,
7233            vec![
7234                PktLineFrame::Data(b"version 2\n".to_vec()),
7235                PktLineFrame::Delimiter,
7236                PktLineFrame::Data(b"done\n".to_vec()),
7237                PktLineFrame::Flush,
7238            ]
7239        );
7240    }
7241
7242    #[test]
7243    fn pkt_line_stream_reads_and_writes_incremental_io() {
7244        let frames = vec![
7245            PktLineFrame::Data(b"version 2\n".to_vec()),
7246            PktLineFrame::Delimiter,
7247            PktLineFrame::Data(b"done\n".to_vec()),
7248            PktLineFrame::Flush,
7249        ];
7250        let mut encoded = Vec::new();
7251        write_pkt_line_frames(&mut encoded, &frames).expect("test operation should succeed");
7252        assert_eq!(encoded, b"000eversion 2\n00010009done\n0000");
7253        assert_eq!(
7254            read_pkt_line_frames(&mut encoded.as_slice()).expect("test operation should succeed"),
7255            frames
7256        );
7257
7258        let mut empty: &[u8] = b"";
7259        assert_eq!(
7260            read_pkt_line_frame(&mut empty).expect("test operation should succeed"),
7261            None
7262        );
7263    }
7264
7265    #[test]
7266    fn pkt_line_stream_reads_until_control_packets() {
7267        let input = b"000eversion 2\n0000trailing";
7268        let frames = read_pkt_line_frames_until_flush(&mut &input[..])
7269            .expect("test operation should succeed");
7270        assert_eq!(
7271            frames,
7272            vec![
7273                PktLineFrame::Data(b"version 2\n".to_vec()),
7274                PktLineFrame::Flush,
7275            ]
7276        );
7277
7278        let input = b"0009done\n0002next";
7279        let frames = read_pkt_line_frames_until_response_end(&mut &input[..])
7280            .expect("test operation should succeed");
7281        assert_eq!(
7282            frames,
7283            vec![
7284                PktLineFrame::Data(b"done\n".to_vec()),
7285                PktLineFrame::ResponseEnd,
7286            ]
7287        );
7288        assert!(read_pkt_line_frames_until_flush(&mut &b"0009done\n"[..]).is_err());
7289    }
7290
7291    #[test]
7292    fn pkt_line_rejects_invalid_lengths() {
7293        assert!(PktLineFrame::parse(b"000").is_err());
7294        assert!(PktLineFrame::parse(b"0003").is_err());
7295        assert!(PktLineFrame::parse(b"000ahello").is_err());
7296        assert!(PktLineFrame::parse(b"zzzz").is_err());
7297        assert!(read_pkt_line_frame(&mut &b"000"[..]).is_err());
7298        assert!(read_pkt_line_frame(&mut &b"0003"[..]).is_err());
7299    }
7300
7301    #[test]
7302    fn pkt_line_rejects_oversized_data() {
7303        let payload = vec![b'x'; PKT_LINE_MAX_PAYLOAD_LEN + 1];
7304        assert!(PktLineFrame::data(payload.clone()).is_err());
7305        assert!(PktLine(payload.clone()).try_encode().is_err());
7306        assert!(PktLineFrame::Data(payload.clone()).try_encode().is_err());
7307        assert!(write_pkt_line_frame(&mut Vec::new(), &PktLineFrame::Data(payload)).is_err());
7308        assert!(PktLineFrame::parse(b"fff1").is_err());
7309    }
7310
7311    #[test]
7312    fn protocol_error_lines_parse_encode_and_stream() {
7313        let error = parse_error_line(b"ERR remote rejected request\n")
7314            .expect("test operation should succeed");
7315        assert_eq!(
7316            error,
7317            ProtocolErrorLine {
7318                message: "remote rejected request".into(),
7319            }
7320        );
7321        assert_eq!(
7322            encode_error_line(&error).expect("test operation should succeed"),
7323            b"ERR remote rejected request\n"
7324        );
7325        assert_eq!(
7326            parse_error_frame(&PktLineFrame::Data(
7327                b"ERR remote rejected request\n".to_vec()
7328            ))
7329            .expect("test operation should succeed"),
7330            Some(error.clone())
7331        );
7332        assert_eq!(
7333            parse_error_frame(&PktLineFrame::Data(b"NAK\n".to_vec()))
7334                .expect("test operation should succeed"),
7335            None
7336        );
7337
7338        let mut encoded = Vec::new();
7339        write_error_line(&mut encoded, &error).expect("test operation should succeed");
7340        encoded.extend_from_slice(b"tail");
7341        let mut input = encoded.as_slice();
7342        assert_eq!(
7343            read_error_line(&mut input).expect("test operation should succeed"),
7344            error
7345        );
7346        assert_eq!(input, b"tail");
7347    }
7348
7349    #[test]
7350    fn protocol_error_lines_reject_malformed_messages() {
7351        assert!(parse_error_line(b"ERR\n").is_err());
7352        assert!(parse_error_line(b"ERR \n").is_err());
7353        assert!(parse_error_line(b"ERR bad\0message\n").is_err());
7354        assert!(parse_error_line(b"NAK\n").is_err());
7355        assert!(
7356            encode_error_line(&ProtocolErrorLine {
7357                message: "bad\nmessage".into(),
7358            })
7359            .is_err()
7360        );
7361        assert!(read_error_line(&mut &b"0000"[..]).is_err());
7362    }
7363
7364    #[test]
7365    fn refspec_parser_handles_fetch_push_and_negative_forms() {
7366        assert_eq!(
7367            parse_refspec("+refs/heads/*:refs/remotes/origin/*")
7368                .expect("test operation should succeed"),
7369            RefSpec {
7370                force: true,
7371                negative: false,
7372                src: Some("refs/heads/*".into()),
7373                dst: Some("refs/remotes/origin/*".into()),
7374                pattern: true,
7375            }
7376        );
7377        assert_eq!(
7378            parse_refspec("refs/heads/main").expect("test operation should succeed"),
7379            RefSpec {
7380                force: false,
7381                negative: false,
7382                src: Some("refs/heads/main".into()),
7383                dst: None,
7384                pattern: false,
7385            }
7386        );
7387        assert_eq!(
7388            parse_refspec(":refs/heads/topic").expect("test operation should succeed"),
7389            RefSpec {
7390                force: false,
7391                negative: false,
7392                src: None,
7393                dst: Some("refs/heads/topic".into()),
7394                pattern: false,
7395            }
7396        );
7397        assert_eq!(
7398            parse_refspec(":").expect("test operation should succeed"),
7399            RefSpec {
7400                force: false,
7401                negative: false,
7402                src: None,
7403                dst: None,
7404                pattern: false,
7405            }
7406        );
7407        assert_eq!(
7408            parse_refspec("^refs/tags/private/*").expect("test operation should succeed"),
7409            RefSpec {
7410                force: false,
7411                negative: true,
7412                src: Some("refs/tags/private/*".into()),
7413                dst: None,
7414                pattern: true,
7415            }
7416        );
7417    }
7418
7419    #[test]
7420    fn refspec_encode_and_map_sources() {
7421        let pattern = parse_refspec("+refs/heads/*:refs/remotes/origin/*")
7422            .expect("test operation should succeed");
7423        assert_eq!(
7424            encode_refspec(&pattern).expect("test operation should succeed"),
7425            "+refs/heads/*:refs/remotes/origin/*"
7426        );
7427        assert!(
7428            refspec_matches_source(&pattern, "refs/heads/main")
7429                .expect("test operation should succeed")
7430        );
7431        assert_eq!(
7432            refspec_map_source(&pattern, "refs/heads/main").expect("test operation should succeed"),
7433            Some("refs/remotes/origin/main".into())
7434        );
7435        assert_eq!(
7436            refspec_map_source(&pattern, "refs/tags/v1").expect("test operation should succeed"),
7437            None
7438        );
7439
7440        let direct = parse_refspec("HEAD:refs/heads/main").expect("test operation should succeed");
7441        assert_eq!(
7442            encode_refspec(&direct).expect("test operation should succeed"),
7443            "HEAD:refs/heads/main"
7444        );
7445        assert_eq!(
7446            refspec_map_source(&direct, "HEAD").expect("test operation should succeed"),
7447            Some("refs/heads/main".into())
7448        );
7449
7450        let delete = parse_refspec(":refs/heads/old").expect("test operation should succeed");
7451        assert_eq!(
7452            encode_refspec(&delete).expect("test operation should succeed"),
7453            ":refs/heads/old"
7454        );
7455        assert_eq!(
7456            refspec_map_source(&delete, "HEAD").expect("test operation should succeed"),
7457            None
7458        );
7459
7460        let matching = parse_refspec(":").expect("test operation should succeed");
7461        assert_eq!(
7462            encode_refspec(&matching).expect("test operation should succeed"),
7463            ":"
7464        );
7465    }
7466
7467    #[test]
7468    fn refspec_parser_rejects_malformed_values() {
7469        assert!(parse_refspec("").is_err());
7470        assert!(parse_refspec("+^refs/heads/main").is_err());
7471        assert!(parse_refspec("^refs/heads/main:refs/remotes/origin/main").is_err());
7472        assert!(parse_refspec("refs/heads/*:refs/remotes/origin/main").is_err());
7473        assert!(parse_refspec("refs/heads/**:refs/remotes/origin/*").is_err());
7474        assert!(parse_refspec("refs/heads/main:refs/remotes/origin/main:extra").is_err());
7475        assert!(parse_refspec("refs/heads/main\n").is_err());
7476        assert!(
7477            encode_refspec(&RefSpec {
7478                force: false,
7479                negative: false,
7480                src: Some("refs/heads/*".into()),
7481                dst: Some("refs/remotes/origin/main".into()),
7482                pattern: true,
7483            })
7484            .is_err()
7485        );
7486    }
7487
7488    #[test]
7489    fn fetch_head_records_parse_encode_and_describe_refs() {
7490        let first = ObjectId::from_hex(
7491            ObjectFormat::Sha1,
7492            "1111111111111111111111111111111111111111",
7493        )
7494        .expect("test operation should succeed");
7495        let second = ObjectId::from_hex(
7496            ObjectFormat::Sha1,
7497            "2222222222222222222222222222222222222222",
7498        )
7499        .expect("test operation should succeed");
7500        let input = b"1111111111111111111111111111111111111111\t\tbranch 'main' of ../bundle.bdl\n2222222222222222222222222222222222222222\tnot-for-merge\ttag 'v1' of ../bundle.bdl\n";
7501        let records =
7502            parse_fetch_head(ObjectFormat::Sha1, input).expect("test operation should succeed");
7503        assert_eq!(
7504            records,
7505            vec![
7506                FetchHeadRecord {
7507                    oid: first,
7508                    not_for_merge: false,
7509                    description: "branch 'main' of ../bundle.bdl".into(),
7510                },
7511                FetchHeadRecord {
7512                    oid: second,
7513                    not_for_merge: true,
7514                    description: "tag 'v1' of ../bundle.bdl".into(),
7515                },
7516            ]
7517        );
7518        assert_eq!(
7519            encode_fetch_head(&records).expect("test operation should succeed"),
7520            input
7521        );
7522        assert_eq!(
7523            parse_fetch_head(ObjectFormat::Sha1, b"").expect("test operation should succeed"),
7524            Vec::<FetchHeadRecord>::new()
7525        );
7526        assert_eq!(
7527            fetch_head_remote_description("refs/heads/main", "../bundle.bdl")
7528                .expect("test operation should succeed"),
7529            "branch 'main' of ../bundle.bdl"
7530        );
7531        assert_eq!(
7532            fetch_head_remote_description("refs/tags/v1", "../bundle.bdl")
7533                .expect("test operation should succeed"),
7534            "tag 'v1' of ../bundle.bdl"
7535        );
7536        assert_eq!(
7537            fetch_head_remote_description("HEAD", "../bundle.bdl")
7538                .expect("test operation should succeed"),
7539            "HEAD of ../bundle.bdl"
7540        );
7541    }
7542
7543    #[test]
7544    fn fetch_head_records_streams_round_trip() {
7545        let records = vec![FetchHeadRecord {
7546            oid: ObjectId::from_hex(
7547                ObjectFormat::Sha1,
7548                "1111111111111111111111111111111111111111",
7549            )
7550            .expect("test operation should succeed"),
7551            not_for_merge: false,
7552            description: "branch 'main' of ../bundle.bdl".into(),
7553        }];
7554        let mut encoded = Vec::new();
7555        write_fetch_head(&mut encoded, &records).expect("test operation should succeed");
7556        let mut input = encoded.as_slice();
7557        assert_eq!(
7558            read_fetch_head(ObjectFormat::Sha1, &mut input).expect("test operation should succeed"),
7559            records
7560        );
7561        assert!(input.is_empty());
7562    }
7563
7564    #[test]
7565    fn fetch_head_records_reject_malformed_lines() {
7566        assert!(
7567            parse_fetch_head(
7568                ObjectFormat::Sha1,
7569                b"1111111111111111111111111111111111111111\t\tbranch 'main'"
7570            )
7571            .is_err()
7572        );
7573        assert!(
7574            parse_fetch_head(
7575                ObjectFormat::Sha1,
7576                b"1111111111111111111111111111111111111111\tfor-merge\tbranch 'main'\n"
7577            )
7578            .is_err()
7579        );
7580        assert!(parse_fetch_head(ObjectFormat::Sha1, b"not-a-hash\t\tbranch 'main'\n").is_err());
7581        assert!(
7582            encode_fetch_head(&[FetchHeadRecord {
7583                oid: ObjectId::from_hex(
7584                    ObjectFormat::Sha1,
7585                    "1111111111111111111111111111111111111111"
7586                )
7587                .expect("test operation should succeed"),
7588                not_for_merge: false,
7589                description: "bad\ndescription".into(),
7590            }])
7591            .is_err()
7592        );
7593    }
7594
7595    #[test]
7596    fn fetch_planner_maps_direct_pattern_and_negative_refspecs() {
7597        let main = ObjectId::from_hex(
7598            ObjectFormat::Sha1,
7599            "1111111111111111111111111111111111111111",
7600        )
7601        .expect("test operation should succeed");
7602        let next = ObjectId::from_hex(
7603            ObjectFormat::Sha1,
7604            "2222222222222222222222222222222222222222",
7605        )
7606        .expect("test operation should succeed");
7607        let refs = vec![
7608            RefAdvertisement {
7609                oid: main.clone(),
7610                name: "refs/heads/main".into(),
7611                capabilities: Vec::new(),
7612            },
7613            RefAdvertisement {
7614                oid: next.clone(),
7615                name: "refs/heads/tmp".into(),
7616                capabilities: Vec::new(),
7617            },
7618        ];
7619        let refspecs = vec![
7620            parse_refspec("+refs/heads/*:refs/remotes/origin/*")
7621                .expect("test operation should succeed"),
7622            parse_refspec("^refs/heads/tmp").expect("test operation should succeed"),
7623        ];
7624        assert_eq!(
7625            plan_fetch_ref_updates(&refs, &refspecs, false).expect("test operation should succeed"),
7626            vec![FetchRefUpdate {
7627                src: "refs/heads/main".into(),
7628                dst: Some("refs/remotes/origin/main".into()),
7629                oid: main,
7630                not_for_merge: false,
7631            }]
7632        );
7633    }
7634
7635    #[test]
7636    fn fetch_planner_autofollows_tags_and_builds_fetch_head_records() {
7637        let commit = ObjectId::from_hex(
7638            ObjectFormat::Sha1,
7639            "1111111111111111111111111111111111111111",
7640        )
7641        .expect("test operation should succeed");
7642        let refs = vec![
7643            RefAdvertisement {
7644                oid: commit.clone(),
7645                name: "refs/heads/main".into(),
7646                capabilities: Vec::new(),
7647            },
7648            RefAdvertisement {
7649                oid: commit.clone(),
7650                name: "refs/tags/v1".into(),
7651                capabilities: Vec::new(),
7652            },
7653        ];
7654        let refspecs = vec![
7655            parse_refspec("refs/heads/main:refs/heads/main")
7656                .expect("test operation should succeed"),
7657        ];
7658        let updates =
7659            plan_fetch_ref_updates(&refs, &refspecs, true).expect("test operation should succeed");
7660        assert_eq!(
7661            updates,
7662            vec![
7663                FetchRefUpdate {
7664                    src: "refs/heads/main".into(),
7665                    dst: Some("refs/heads/main".into()),
7666                    oid: commit.clone(),
7667                    not_for_merge: false,
7668                },
7669                FetchRefUpdate {
7670                    src: "refs/tags/v1".into(),
7671                    dst: Some("refs/tags/v1".into()),
7672                    oid: commit.clone(),
7673                    not_for_merge: true,
7674                },
7675            ]
7676        );
7677        assert_eq!(
7678            fetch_ref_updates_to_fetch_head(&updates, "../bundle.bdl")
7679                .expect("test operation should succeed"),
7680            vec![
7681                FetchHeadRecord {
7682                    oid: commit.clone(),
7683                    not_for_merge: false,
7684                    description: "branch 'main' of ../bundle.bdl".into(),
7685                },
7686                FetchHeadRecord {
7687                    oid: commit,
7688                    not_for_merge: true,
7689                    description: "tag 'v1' of ../bundle.bdl".into(),
7690                },
7691            ]
7692        );
7693    }
7694
7695    #[test]
7696    fn fetch_planner_rejects_missing_or_sourceless_refspecs() {
7697        let refs = vec![RefAdvertisement {
7698            oid: ObjectId::from_hex(
7699                ObjectFormat::Sha1,
7700                "1111111111111111111111111111111111111111",
7701            )
7702            .expect("test operation should succeed"),
7703            name: "refs/heads/main".into(),
7704            capabilities: Vec::new(),
7705        }];
7706        assert!(
7707            plan_fetch_ref_updates(
7708                &refs,
7709                &[parse_refspec("refs/heads/missing").expect("test operation should succeed")],
7710                false
7711            )
7712            .is_err()
7713        );
7714        assert!(
7715            plan_fetch_ref_updates(
7716                &refs,
7717                &[parse_refspec(":refs/heads/main").expect("test operation should succeed")],
7718                false
7719            )
7720            .is_err()
7721        );
7722    }
7723
7724    #[test]
7725    fn fetch_planner_sourceless_positive_refspec_returns_err_not_panic() {
7726        // Regression guard for the sley#7 conversion at the `let Some(src) = ..`
7727        // binding: a non-pattern positive refspec with no source must return an
7728        // error, never panic. Construct the malformed RefSpec directly so the
7729        // test pins the converted guard rather than parse_refspec's behavior.
7730        let refs = vec![RefAdvertisement {
7731            oid: ObjectId::from_hex(
7732                ObjectFormat::Sha1,
7733                "1111111111111111111111111111111111111111",
7734            )
7735            .expect("test operation should succeed"),
7736            name: "refs/heads/main".into(),
7737            capabilities: Vec::new(),
7738        }];
7739        let malformed = RefSpec {
7740            force: false,
7741            negative: false,
7742            src: None,
7743            dst: Some("refs/heads/main".into()),
7744            pattern: false,
7745        };
7746        let result = plan_fetch_ref_updates(&refs, &[malformed], false);
7747        assert!(
7748            result.is_err(),
7749            "sourceless positive refspec must yield Err, got {result:?}"
7750        );
7751    }
7752
7753    #[test]
7754    fn push_planner_builds_create_update_delete_and_matching_commands() {
7755        let old = ObjectId::from_hex(
7756            ObjectFormat::Sha1,
7757            "1111111111111111111111111111111111111111",
7758        )
7759        .expect("test operation should succeed");
7760        let new = ObjectId::from_hex(
7761            ObjectFormat::Sha1,
7762            "2222222222222222222222222222222222222222",
7763        )
7764        .expect("test operation should succeed");
7765        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
7766        let local_refs = vec![
7767            PushSourceRef {
7768                oid: new.clone(),
7769                name: "refs/heads/main".into(),
7770            },
7771            PushSourceRef {
7772                oid: new.clone(),
7773                name: "refs/heads/new".into(),
7774            },
7775        ];
7776        let remote_refs = vec![
7777            RefAdvertisement {
7778                oid: old.clone(),
7779                name: "refs/heads/main".into(),
7780                capabilities: Vec::new(),
7781            },
7782            RefAdvertisement {
7783                oid: old.clone(),
7784                name: "refs/heads/old".into(),
7785                capabilities: Vec::new(),
7786            },
7787        ];
7788
7789        assert_eq!(
7790            plan_push_commands(
7791                ObjectFormat::Sha1,
7792                &local_refs,
7793                &remote_refs,
7794                &[parse_refspec("refs/heads/main").expect("test operation should succeed")],
7795            )
7796            .expect("test operation should succeed"),
7797            vec![ReceivePackCommand {
7798                old_id: old.clone(),
7799                new_id: new.clone(),
7800                name: "refs/heads/main".into(),
7801            }]
7802        );
7803        assert_eq!(
7804            plan_push_commands(
7805                ObjectFormat::Sha1,
7806                &local_refs,
7807                &remote_refs,
7808                &[parse_refspec("refs/heads/new:refs/heads/new")
7809                    .expect("test operation should succeed")],
7810            )
7811            .expect("test operation should succeed"),
7812            vec![ReceivePackCommand {
7813                old_id: zero.clone(),
7814                new_id: new.clone(),
7815                name: "refs/heads/new".into(),
7816            }]
7817        );
7818        assert_eq!(
7819            plan_push_commands(
7820                ObjectFormat::Sha1,
7821                &local_refs,
7822                &remote_refs,
7823                &[parse_refspec(":refs/heads/old").expect("test operation should succeed")],
7824            )
7825            .expect("test operation should succeed"),
7826            vec![ReceivePackCommand {
7827                old_id: old.clone(),
7828                new_id: zero,
7829                name: "refs/heads/old".into(),
7830            }]
7831        );
7832        assert_eq!(
7833            plan_push_commands(
7834                ObjectFormat::Sha1,
7835                &local_refs,
7836                &remote_refs,
7837                &[parse_refspec(":").expect("test operation should succeed")],
7838            )
7839            .expect("test operation should succeed"),
7840            vec![ReceivePackCommand {
7841                old_id: old,
7842                new_id: new,
7843                name: "refs/heads/main".into(),
7844            }]
7845        );
7846    }
7847
7848    #[test]
7849    fn push_planner_builds_wildcard_commands_and_rejects_bad_refspecs() {
7850        let new = ObjectId::from_hex(
7851            ObjectFormat::Sha1,
7852            "2222222222222222222222222222222222222222",
7853        )
7854        .expect("test operation should succeed");
7855        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
7856        let local_refs = vec![PushSourceRef {
7857            oid: new.clone(),
7858            name: "refs/heads/topic".into(),
7859        }];
7860        let commands = plan_push_commands(
7861            ObjectFormat::Sha1,
7862            &local_refs,
7863            &[],
7864            &[parse_refspec("refs/heads/*:refs/heads/review/*")
7865                .expect("test operation should succeed")],
7866        )
7867        .expect("test operation should succeed");
7868        assert_eq!(
7869            commands,
7870            vec![ReceivePackCommand {
7871                old_id: zero,
7872                new_id: new,
7873                name: "refs/heads/review/topic".into(),
7874            }]
7875        );
7876        assert!(
7877            plan_push_commands(
7878                ObjectFormat::Sha1,
7879                &local_refs,
7880                &[],
7881                &[parse_refspec("^refs/heads/topic").expect("test operation should succeed")],
7882            )
7883            .is_err()
7884        );
7885        assert!(
7886            plan_push_commands(
7887                ObjectFormat::Sha1,
7888                &local_refs,
7889                &[],
7890                &[parse_refspec(":refs/heads/missing").expect("test operation should succeed")],
7891            )
7892            .is_err()
7893        );
7894    }
7895
7896    #[test]
7897    fn receive_pack_push_request_builder_negotiates_capabilities() {
7898        let old_id = ObjectId::from_hex(
7899            ObjectFormat::Sha1,
7900            "1111111111111111111111111111111111111111",
7901        )
7902        .expect("test operation should succeed");
7903        let new_id = ObjectId::from_hex(
7904            ObjectFormat::Sha1,
7905            "2222222222222222222222222222222222222222",
7906        )
7907        .expect("test operation should succeed");
7908        let features = ReceivePackFeatures {
7909            report_status_v2: true,
7910            atomic: true,
7911            ofs_delta: true,
7912            push_options: true,
7913            side_band_64k: true,
7914            quiet: true,
7915            object_format: Some(ObjectFormat::Sha1),
7916            ..ReceivePackFeatures::default()
7917        };
7918        let request = build_receive_pack_push_request(
7919            &features,
7920            vec![ReceivePackCommand {
7921                old_id,
7922                new_id,
7923                name: "refs/heads/main".into(),
7924            }],
7925            b"PACKdata".to_vec(),
7926            ReceivePackPushRequestOptions {
7927                report_status_v2: true,
7928                atomic: true,
7929                ofs_delta: true,
7930                side_band_64k: true,
7931                quiet: true,
7932                agent: Some("sley/0".into()),
7933                object_format: Some(ObjectFormat::Sha1),
7934                push_options: vec!["ci.skip".into()],
7935                ..ReceivePackPushRequestOptions::default()
7936            },
7937        )
7938        .expect("test operation should succeed");
7939        assert_eq!(
7940            request.commands.capabilities,
7941            vec![
7942                Capability {
7943                    name: "report-status-v2".into(),
7944                    value: None,
7945                },
7946                Capability {
7947                    name: "atomic".into(),
7948                    value: None,
7949                },
7950                Capability {
7951                    name: "ofs-delta".into(),
7952                    value: None,
7953                },
7954                Capability {
7955                    name: "side-band-64k".into(),
7956                    value: None,
7957                },
7958                Capability {
7959                    name: "quiet".into(),
7960                    value: None,
7961                },
7962                Capability {
7963                    name: "agent".into(),
7964                    value: Some("sley/0".into()),
7965                },
7966                Capability {
7967                    name: "object-format".into(),
7968                    value: Some("sha1".into()),
7969                },
7970                Capability {
7971                    name: "push-options".into(),
7972                    value: None,
7973                },
7974            ]
7975        );
7976        assert_eq!(request.push_options, Some(vec!["ci.skip".into()]));
7977        validate_receive_pack_push_request_features(&features, &request)
7978            .expect("test operation should succeed");
7979    }
7980
7981    #[test]
7982    fn receive_pack_push_request_builder_handles_delete_only_and_rejects_unadvertised() {
7983        let old_id = ObjectId::from_hex(
7984            ObjectFormat::Sha1,
7985            "1111111111111111111111111111111111111111",
7986        )
7987        .expect("test operation should succeed");
7988        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
7989        let features = ReceivePackFeatures {
7990            delete_refs: true,
7991            ..ReceivePackFeatures::default()
7992        };
7993        let request = build_receive_pack_push_request(
7994            &features,
7995            vec![ReceivePackCommand {
7996                old_id,
7997                new_id: zero,
7998                name: "refs/heads/old".into(),
7999            }],
8000            Vec::new(),
8001            ReceivePackPushRequestOptions::default(),
8002        )
8003        .expect("test operation should succeed");
8004        assert_eq!(
8005            request.commands.capabilities,
8006            vec![Capability {
8007                name: "delete-refs".into(),
8008                value: None,
8009            }]
8010        );
8011        assert!(request.packfile.is_empty());
8012
8013        assert!(
8014            build_receive_pack_push_request(
8015                &ReceivePackFeatures::default(),
8016                request.commands.commands.clone(),
8017                Vec::new(),
8018                ReceivePackPushRequestOptions::default(),
8019            )
8020            .is_err()
8021        );
8022        assert!(
8023            build_receive_pack_push_request(
8024                &features,
8025                request.commands.commands,
8026                b"PACK".to_vec(),
8027                ReceivePackPushRequestOptions::default(),
8028            )
8029            .is_err()
8030        );
8031        assert!(
8032            build_receive_pack_push_request(
8033                &features,
8034                Vec::new(),
8035                Vec::new(),
8036                ReceivePackPushRequestOptions {
8037                    push_options: vec!["ci.skip".into()],
8038                    ..ReceivePackPushRequestOptions::default()
8039                },
8040            )
8041            .is_err()
8042        );
8043    }
8044
8045    #[test]
8046    fn smart_http_helpers_build_paths_and_content_types() {
8047        let sha1 = ObjectId::from_hex(
8048            ObjectFormat::Sha1,
8049            "1111111111111111111111111111111111111111",
8050        )
8051        .expect("test operation should succeed");
8052        let sha256 = ObjectId::from_hex(
8053            ObjectFormat::Sha256,
8054            "2222222222222222222222222222222222222222222222222222222222222222",
8055        )
8056        .expect("test operation should succeed");
8057        assert_eq!(
8058            smart_http_info_refs_path("/repo.git/", GitService::UploadPack)
8059                .expect("test operation should succeed"),
8060            "/repo.git/info/refs?service=git-upload-pack"
8061        );
8062        assert_eq!(
8063            dumb_http_info_refs_path("/repo.git/").expect("test operation should succeed"),
8064            "/repo.git/info/refs"
8065        );
8066        assert_eq!(
8067            dumb_http_alternates_path("/repo.git").expect("test operation should succeed"),
8068            "/repo.git/objects/info/http-alternates"
8069        );
8070        assert_eq!(
8071            dumb_http_packs_path("/repo.git/").expect("test operation should succeed"),
8072            "/repo.git/objects/info/packs"
8073        );
8074        assert_eq!(
8075            dumb_http_loose_object_path("/repo.git/", &sha1)
8076                .expect("test operation should succeed"),
8077            "/repo.git/objects/11/11111111111111111111111111111111111111"
8078        );
8079        assert_eq!(
8080            dumb_http_loose_object_path("/repo.git/", &sha256)
8081                .expect("test operation should succeed"),
8082            "/repo.git/objects/22/22222222222222222222222222222222222222222222222222222222222222"
8083        );
8084        assert_eq!(
8085            dumb_http_pack_file_path("/repo.git/", &sha1).expect("test operation should succeed"),
8086            "/repo.git/objects/pack/pack-1111111111111111111111111111111111111111.pack"
8087        );
8088        assert_eq!(
8089            dumb_http_pack_index_path("/repo.git/", &sha1).expect("test operation should succeed"),
8090            "/repo.git/objects/pack/pack-1111111111111111111111111111111111111111.idx"
8091        );
8092        assert_eq!(
8093            smart_http_rpc_path("/repo.git", GitService::ReceivePack)
8094                .expect("test operation should succeed"),
8095            "/repo.git/git-receive-pack"
8096        );
8097        assert_eq!(
8098            smart_http_advertisement_content_type(GitService::UploadPack)
8099                .expect("test operation should succeed"),
8100            "application/x-git-upload-pack-advertisement"
8101        );
8102        assert_eq!(
8103            smart_http_rpc_request_content_type(GitService::UploadPack)
8104                .expect("test operation should succeed"),
8105            "application/x-git-upload-pack-request"
8106        );
8107        assert_eq!(
8108            smart_http_rpc_result_content_type(GitService::ReceivePack)
8109                .expect("test operation should succeed"),
8110            "application/x-git-receive-pack-result"
8111        );
8112        assert_eq!(
8113            parse_smart_http_advertisement_content_type(
8114                "Application/X-Git-Upload-Pack-Advertisement"
8115            )
8116            .expect("test operation should succeed"),
8117            GitService::UploadPack
8118        );
8119        assert_eq!(
8120            parse_smart_http_rpc_request_content_type("application/x-git-receive-pack-request")
8121                .expect("test operation should succeed"),
8122            GitService::ReceivePack
8123        );
8124        assert_eq!(
8125            parse_smart_http_rpc_result_content_type("application/x-git-upload-pack-result")
8126                .expect("test operation should succeed"),
8127            GitService::UploadPack
8128        );
8129    }
8130
8131    #[test]
8132    fn smart_http_helpers_reject_invalid_services_paths_and_content_types() {
8133        let oid = ObjectId::from_hex(
8134            ObjectFormat::Sha1,
8135            "1111111111111111111111111111111111111111",
8136        )
8137        .expect("test operation should succeed");
8138        assert!(smart_http_info_refs_path("repo.git", GitService::UploadPack).is_err());
8139        assert!(smart_http_rpc_path("/repo.git?x=1", GitService::UploadPack).is_err());
8140        assert!(dumb_http_info_refs_path("repo.git").is_err());
8141        assert!(dumb_http_alternates_path("/repo.git#fragment").is_err());
8142        assert!(dumb_http_packs_path("/repo.git?query").is_err());
8143        assert!(dumb_http_loose_object_path("repo.git", &oid).is_err());
8144        assert!(dumb_http_pack_file_path("/repo.git#fragment", &oid).is_err());
8145        assert!(dumb_http_pack_index_path("/repo.git?query", &oid).is_err());
8146        assert!(smart_http_info_refs_path("/repo.git", GitService::UploadArchive).is_err());
8147        assert!(smart_http_advertisement_content_type(GitService::UploadArchive).is_err());
8148        assert!(
8149            parse_smart_http_advertisement_content_type(
8150                "application/x-git-upload-archive-advertisement"
8151            )
8152            .is_err()
8153        );
8154        assert!(
8155            parse_smart_http_rpc_request_content_type("application/x-git-upload-pack-result")
8156                .is_err()
8157        );
8158        assert!(
8159            parse_smart_http_rpc_result_content_type(
8160                "application/x-git-receive-pack-result; charset=utf-8"
8161            )
8162            .is_err()
8163        );
8164    }
8165
8166    #[test]
8167    fn sideband_packets_parse_and_encode_channels() {
8168        let payloads = vec![
8169            b"\x01PACK bytes".to_vec(),
8170            b"\x02counting objects\n".to_vec(),
8171            b"\x03fatal error\n".to_vec(),
8172        ];
8173        let packets = parse_sideband_packets(&payloads).expect("test operation should succeed");
8174        assert_eq!(
8175            packets,
8176            vec![
8177                SideBandPacket {
8178                    channel: SideBandChannel::Data,
8179                    data: b"PACK bytes".to_vec(),
8180                },
8181                SideBandPacket {
8182                    channel: SideBandChannel::Progress,
8183                    data: b"counting objects\n".to_vec(),
8184                },
8185                SideBandPacket {
8186                    channel: SideBandChannel::Fatal,
8187                    data: b"fatal error\n".to_vec(),
8188                },
8189            ]
8190        );
8191        assert_eq!(
8192            encode_sideband_packets(&packets).expect("test operation should succeed"),
8193            payloads
8194        );
8195    }
8196
8197    #[test]
8198    fn sideband_stream_parses_encodes_and_demuxes_packets() {
8199        let frames = vec![
8200            PktLineFrame::Data(vec![1, b'P', b'A']),
8201            PktLineFrame::Data(vec![2, b'c', b'o', b'u', b'n', b't', b'\n']),
8202            PktLineFrame::Data(vec![1, b'C', b'K']),
8203            PktLineFrame::Flush,
8204        ];
8205        let packets = parse_sideband_stream(&frames).expect("test operation should succeed");
8206        assert_eq!(
8207            packets,
8208            vec![
8209                SideBandPacket {
8210                    channel: SideBandChannel::Data,
8211                    data: b"PA".to_vec(),
8212                },
8213                SideBandPacket {
8214                    channel: SideBandChannel::Progress,
8215                    data: b"count\n".to_vec(),
8216                },
8217                SideBandPacket {
8218                    channel: SideBandChannel::Data,
8219                    data: b"CK".to_vec(),
8220                },
8221            ]
8222        );
8223        assert_eq!(
8224            encode_sideband_stream(&packets).expect("test operation should succeed"),
8225            frames
8226        );
8227        assert_eq!(
8228            demux_sideband_stream(&frames).expect("test operation should succeed"),
8229            SideBandDemux {
8230                data: b"PACK".to_vec(),
8231                progress: vec![b"count\n".to_vec()],
8232            }
8233        );
8234    }
8235
8236    #[test]
8237    fn sideband_stream_reads_and_writes_until_flush() {
8238        let packets = vec![
8239            SideBandPacket {
8240                channel: SideBandChannel::Data,
8241                data: b"PACK".to_vec(),
8242            },
8243            SideBandPacket {
8244                channel: SideBandChannel::Progress,
8245                data: b"done\n".to_vec(),
8246            },
8247        ];
8248        let mut encoded = Vec::new();
8249        write_sideband_stream(&mut encoded, &packets).expect("test operation should succeed");
8250        encoded.extend_from_slice(b"tail");
8251
8252        let mut input = encoded.as_slice();
8253        assert_eq!(
8254            read_sideband_stream(&mut input).expect("test operation should succeed"),
8255            packets
8256        );
8257        assert_eq!(input, b"tail");
8258
8259        let mut input = encoded.as_slice();
8260        assert_eq!(
8261            read_and_demux_sideband_stream(&mut input).expect("test operation should succeed"),
8262            SideBandDemux {
8263                data: b"PACK".to_vec(),
8264                progress: vec![b"done\n".to_vec()],
8265            }
8266        );
8267        assert_eq!(input, b"tail");
8268    }
8269
8270    #[test]
8271    fn sideband_packets_demux_data_and_progress() {
8272        let payloads = vec![
8273            b"\x01PACK".to_vec(),
8274            b"\x02counting objects\n".to_vec(),
8275            b"\x01 bytes".to_vec(),
8276            b"\x02done\n".to_vec(),
8277        ];
8278        assert_eq!(
8279            parse_and_demux_sideband_packets(&payloads).expect("test operation should succeed"),
8280            SideBandDemux {
8281                data: b"PACK bytes".to_vec(),
8282                progress: vec![b"counting objects\n".to_vec(), b"done\n".to_vec()],
8283            }
8284        );
8285    }
8286
8287    #[test]
8288    fn sideband_packets_reject_bad_channels_and_oversize_payloads() {
8289        assert!(parse_sideband_packet(b"").is_err());
8290        assert!(parse_sideband_packet(b"\x04bad").is_err());
8291        assert!(
8292            parse_sideband_stream(&[PktLineFrame::Data(vec![1, b'P', b'A', b'C', b'K'])]).is_err()
8293        );
8294        assert!(parse_sideband_stream(&[PktLineFrame::Delimiter, PktLineFrame::Flush]).is_err());
8295        assert!(
8296            parse_sideband_stream(&[
8297                PktLineFrame::Data(vec![1, b'P', b'A']),
8298                PktLineFrame::Flush,
8299                PktLineFrame::Data(vec![1, b'C', b'K']),
8300            ])
8301            .is_err()
8302        );
8303        assert!(
8304            parse_sideband_stream(&[
8305                PktLineFrame::Data(vec![1, b'P', b'A']),
8306                PktLineFrame::Data(b"\x04bad".to_vec()),
8307                PktLineFrame::Flush,
8308            ])
8309            .is_err()
8310        );
8311        assert!(
8312            encode_sideband_packet(&SideBandPacket {
8313                channel: SideBandChannel::Data,
8314                data: vec![0; PKT_LINE_MAX_PAYLOAD_LEN],
8315            })
8316            .is_err()
8317        );
8318        assert!(
8319            demux_sideband_packets(&[SideBandPacket {
8320                channel: SideBandChannel::Fatal,
8321                data: b"remote died\n".to_vec(),
8322            }])
8323            .is_err()
8324        );
8325    }
8326
8327    #[test]
8328    fn upload_archive_request_parses_and_encodes_arguments() {
8329        let frames = vec![
8330            PktLineFrame::Data(b"argument --format=tar\n".to_vec()),
8331            PktLineFrame::Data(b"argument HEAD:dir with spaces\n".to_vec()),
8332            PktLineFrame::Flush,
8333        ];
8334        let request = parse_upload_archive_request(&frames).expect("test operation should succeed");
8335        assert_eq!(
8336            request,
8337            UploadArchiveRequest {
8338                arguments: vec!["--format=tar".into(), "HEAD:dir with spaces".into()],
8339            }
8340        );
8341        assert_eq!(
8342            encode_upload_archive_request(&request).expect("test operation should succeed"),
8343            frames
8344        );
8345    }
8346
8347    #[test]
8348    fn upload_archive_request_streams_round_trip() {
8349        let request = UploadArchiveRequest {
8350            arguments: vec!["--prefix=src/".into(), "main".into()],
8351        };
8352        let mut encoded = Vec::new();
8353        write_upload_archive_request(&mut encoded, &request)
8354            .expect("test operation should succeed");
8355        encoded.extend_from_slice(b"tail");
8356
8357        let mut input = encoded.as_slice();
8358        assert_eq!(
8359            read_upload_archive_request(&mut input).expect("test operation should succeed"),
8360            request
8361        );
8362        assert_eq!(input, b"tail");
8363    }
8364
8365    #[test]
8366    fn upload_archive_request_rejects_malformed_streams() {
8367        assert!(parse_upload_archive_request(&[PktLineFrame::Flush]).is_err());
8368        assert!(
8369            parse_upload_archive_request(&[
8370                PktLineFrame::Data(b"--format=tar\n".to_vec()),
8371                PktLineFrame::Flush,
8372            ])
8373            .is_err()
8374        );
8375        assert!(
8376            parse_upload_archive_request(&[
8377                PktLineFrame::Data(b"argument HEAD\n".to_vec()),
8378                PktLineFrame::Delimiter,
8379                PktLineFrame::Flush,
8380            ])
8381            .is_err()
8382        );
8383        assert!(
8384            encode_upload_archive_request(&UploadArchiveRequest {
8385                arguments: vec!["bad\narg".into()],
8386            })
8387            .is_err()
8388        );
8389    }
8390
8391    #[test]
8392    fn upload_archive_response_parses_ack_sideband_and_nack() {
8393        let ack_frames = vec![
8394            PktLineFrame::Data(b"ACK\n".to_vec()),
8395            PktLineFrame::Data(b"\x01tar bytes".to_vec()),
8396            PktLineFrame::Data(b"\x02progress\n".to_vec()),
8397            PktLineFrame::Flush,
8398        ];
8399        let response =
8400            parse_upload_archive_response(&ack_frames).expect("test operation should succeed");
8401        assert_eq!(
8402            response,
8403            UploadArchiveResponse::Ack {
8404                sideband: vec![
8405                    SideBandPacket {
8406                        channel: SideBandChannel::Data,
8407                        data: b"tar bytes".to_vec(),
8408                    },
8409                    SideBandPacket {
8410                        channel: SideBandChannel::Progress,
8411                        data: b"progress\n".to_vec(),
8412                    },
8413                ],
8414            }
8415        );
8416        assert_eq!(
8417            encode_upload_archive_response(&response).expect("test operation should succeed"),
8418            ack_frames
8419        );
8420        assert_eq!(
8421            demux_upload_archive_response(&response).expect("test operation should succeed"),
8422            SideBandDemux {
8423                data: b"tar bytes".to_vec(),
8424                progress: vec![b"progress\n".to_vec()],
8425            }
8426        );
8427
8428        let nack = UploadArchiveResponse::Nack {
8429            message: "unreachable tree".into(),
8430        };
8431        let nack_frames = vec![
8432            PktLineFrame::Data(b"NACK unreachable tree\n".to_vec()),
8433            PktLineFrame::Flush,
8434        ];
8435        assert_eq!(
8436            parse_upload_archive_response(&nack_frames).expect("test operation should succeed"),
8437            nack
8438        );
8439        assert_eq!(
8440            encode_upload_archive_response(&nack).expect("test operation should succeed"),
8441            nack_frames
8442        );
8443        assert!(demux_upload_archive_response(&nack).is_err());
8444    }
8445
8446    #[test]
8447    fn upload_archive_response_streams_round_trip() {
8448        let response = UploadArchiveResponse::Ack {
8449            sideband: vec![SideBandPacket {
8450                channel: SideBandChannel::Data,
8451                data: b"tar bytes".to_vec(),
8452            }],
8453        };
8454        let mut encoded = Vec::new();
8455        write_upload_archive_response(&mut encoded, &response)
8456            .expect("test operation should succeed");
8457        encoded.extend_from_slice(b"tail");
8458
8459        let mut input = encoded.as_slice();
8460        assert_eq!(
8461            read_upload_archive_response(&mut input).expect("test operation should succeed"),
8462            response
8463        );
8464        assert_eq!(input, b"tail");
8465    }
8466
8467    #[test]
8468    fn upload_archive_response_rejects_malformed_streams() {
8469        assert!(parse_upload_archive_response(&[]).is_err());
8470        assert!(
8471            parse_upload_archive_response(&[
8472                PktLineFrame::Data(b"ACK\n".to_vec()),
8473                PktLineFrame::Flush,
8474                PktLineFrame::Data(b"\x01tail".to_vec()),
8475            ])
8476            .is_err()
8477        );
8478        assert!(
8479            parse_upload_archive_response(&[
8480                PktLineFrame::Data(b"NACK\n".to_vec()),
8481                PktLineFrame::Flush,
8482            ])
8483            .is_err()
8484        );
8485        assert!(
8486            parse_upload_archive_response(&[
8487                PktLineFrame::Data(b"NACK denied\n".to_vec()),
8488                PktLineFrame::Data(b"\x02extra\n".to_vec()),
8489                PktLineFrame::Flush,
8490            ])
8491            .is_err()
8492        );
8493        assert!(
8494            encode_upload_archive_response(&UploadArchiveResponse::Nack {
8495                message: "bad\nmessage".into(),
8496            })
8497            .is_err()
8498        );
8499    }
8500
8501    #[test]
8502    fn capabilities_parse_and_encode_tokens() {
8503        let capabilities = parse_capabilities(
8504            b"multi_ack thin-pack agent=git/2.54.0 symref=HEAD:refs/heads/main\n",
8505        )
8506        .expect("test operation should succeed");
8507        assert_eq!(
8508            capabilities,
8509            vec![
8510                Capability {
8511                    name: "multi_ack".into(),
8512                    value: None,
8513                },
8514                Capability {
8515                    name: "thin-pack".into(),
8516                    value: None,
8517                },
8518                Capability {
8519                    name: "agent".into(),
8520                    value: Some("git/2.54.0".into()),
8521                },
8522                Capability {
8523                    name: "symref".into(),
8524                    value: Some("HEAD:refs/heads/main".into()),
8525                },
8526            ]
8527        );
8528        assert_eq!(
8529            encode_capabilities(&capabilities).expect("test operation should succeed"),
8530            b"multi_ack thin-pack agent=git/2.54.0 symref=HEAD:refs/heads/main"
8531        );
8532    }
8533
8534    #[test]
8535    fn capabilities_reject_empty_or_delimited_fields() {
8536        assert!(parse_capabilities(b"multi_ack  thin-pack").is_err());
8537        assert!(parse_capabilities(b"agent=").is_err());
8538        assert!(parse_capabilities(b"symref=HEAD:refs/heads/main\nbad").is_err());
8539        assert!(
8540            encode_capabilities(&[Capability {
8541                name: "bad name".into(),
8542                value: None,
8543            }])
8544            .is_err()
8545        );
8546    }
8547
8548    #[test]
8549    fn protocol_v2_object_format_uses_capability_or_defaults_to_sha1() {
8550        assert_eq!(
8551            protocol_v2_object_format(&[]).expect("test operation should succeed"),
8552            ObjectFormat::Sha1
8553        );
8554        assert_eq!(
8555            protocol_v2_object_format(&[Capability {
8556                name: "object-format".into(),
8557                value: Some("sha256".into()),
8558            }])
8559            .expect("test operation should succeed"),
8560            ObjectFormat::Sha256
8561        );
8562        assert!(
8563            protocol_v2_object_format(&[Capability {
8564                name: "object-format".into(),
8565                value: None,
8566            }])
8567            .is_err()
8568        );
8569        assert!(
8570            protocol_v2_object_format(&[
8571                Capability {
8572                    name: "object-format".into(),
8573                    value: Some("sha1".into()),
8574                },
8575                Capability {
8576                    name: "object-format".into(),
8577                    value: Some("sha256".into()),
8578                },
8579            ])
8580            .is_err()
8581        );
8582        assert!(
8583            protocol_v2_object_format(&[Capability {
8584                name: "object-format".into(),
8585                value: Some("unknown".into()),
8586            }])
8587            .is_err()
8588        );
8589    }
8590
8591    #[test]
8592    fn protocol_v2_command_request_capabilities_validate_against_handshake() {
8593        let handshake = TransportHandshake {
8594            protocol: ProtocolVersion::V2,
8595            capabilities: vec![
8596                Capability {
8597                    name: "fetch".into(),
8598                    value: Some("shallow filter".into()),
8599                },
8600                Capability {
8601                    name: "agent".into(),
8602                    value: Some("sley/0".into()),
8603                },
8604                Capability {
8605                    name: "object-format".into(),
8606                    value: Some("sha1".into()),
8607                },
8608            ],
8609        };
8610        validate_protocol_v2_command_request_capabilities(
8611            &handshake,
8612            &ProtocolV2CommandRequest {
8613                command: "fetch".into(),
8614                capabilities: vec![
8615                    Capability {
8616                        name: "agent".into(),
8617                        value: Some("client/1".into()),
8618                    },
8619                    Capability {
8620                        name: "object-format".into(),
8621                        value: Some("sha1".into()),
8622                    },
8623                ],
8624                arguments: Vec::new(),
8625            },
8626        )
8627        .expect("test operation should succeed");
8628        assert!(
8629            validate_protocol_v2_command_request_capabilities(
8630                &handshake,
8631                &ProtocolV2CommandRequest {
8632                    command: "ls-refs".into(),
8633                    capabilities: Vec::new(),
8634                    arguments: Vec::new(),
8635                },
8636            )
8637            .is_err()
8638        );
8639        assert!(
8640            validate_protocol_v2_command_request_capabilities(
8641                &handshake,
8642                &ProtocolV2CommandRequest {
8643                    command: "fetch".into(),
8644                    capabilities: vec![Capability {
8645                        name: "server-option".into(),
8646                        value: None,
8647                    }],
8648                    arguments: Vec::new(),
8649                },
8650            )
8651            .is_err()
8652        );
8653        assert!(
8654            validate_protocol_v2_command_request_capabilities(
8655                &handshake,
8656                &ProtocolV2CommandRequest {
8657                    command: "fetch".into(),
8658                    capabilities: vec![Capability {
8659                        name: "object-format".into(),
8660                        value: Some("sha256".into()),
8661                    }],
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: "agent".into(),
8674                        value: None,
8675                    }],
8676                    arguments: Vec::new(),
8677                },
8678            )
8679            .is_err()
8680        );
8681    }
8682
8683    #[test]
8684    fn protocol_v2_command_options_parse_and_encode_known_capabilities() {
8685        let capabilities = vec![
8686            Capability {
8687                name: "agent".into(),
8688                value: Some("sley/0".into()),
8689            },
8690            Capability {
8691                name: "object-format".into(),
8692                value: Some("sha256".into()),
8693            },
8694            Capability {
8695                name: "server-option".into(),
8696                value: Some("trace=true".into()),
8697            },
8698            Capability {
8699                name: "server-option".into(),
8700                value: Some("region=west".into()),
8701            },
8702            Capability {
8703                name: "session-id".into(),
8704                value: Some("abc123".into()),
8705            },
8706        ];
8707        let options = parse_protocol_v2_command_options(&capabilities)
8708            .expect("test operation should succeed");
8709        assert_eq!(
8710            options,
8711            ProtocolV2CommandOptions {
8712                agent: Some("sley/0".into()),
8713                object_format: Some(ObjectFormat::Sha256),
8714                server_options: vec!["trace=true".into(), "region=west".into()],
8715                extra: vec![Capability {
8716                    name: "session-id".into(),
8717                    value: Some("abc123".into()),
8718                }],
8719            }
8720        );
8721        assert_eq!(
8722            encode_protocol_v2_command_options(&options).expect("test operation should succeed"),
8723            capabilities
8724        );
8725    }
8726
8727    #[test]
8728    fn protocol_v2_command_options_reject_malformed_known_capabilities() {
8729        assert!(
8730            parse_protocol_v2_command_options(&[
8731                Capability {
8732                    name: "agent".into(),
8733                    value: Some("sley/0".into()),
8734                },
8735                Capability {
8736                    name: "agent".into(),
8737                    value: Some("sley/1".into()),
8738                },
8739            ])
8740            .is_err()
8741        );
8742        assert!(
8743            parse_protocol_v2_command_options(&[Capability {
8744                name: "object-format".into(),
8745                value: Some("sha512".into()),
8746            }])
8747            .is_err()
8748        );
8749        assert!(
8750            parse_protocol_v2_command_options(&[Capability {
8751                name: "server-option".into(),
8752                value: None,
8753            }])
8754            .is_err()
8755        );
8756        assert!(
8757            encode_protocol_v2_command_options(&ProtocolV2CommandOptions {
8758                extra: vec![Capability {
8759                    name: "server-option".into(),
8760                    value: Some("trace=true".into()),
8761                }],
8762                ..ProtocolV2CommandOptions::default()
8763            })
8764            .is_err()
8765        );
8766    }
8767
8768    #[test]
8769    fn protocol_v2_ls_refs_features_parse_and_encode_advertisement() {
8770        let capabilities = vec![Capability {
8771            name: "ls-refs".into(),
8772            value: Some("unborn custom".into()),
8773        }];
8774        let features = parse_protocol_v2_ls_refs_features(&capabilities)
8775            .expect("test operation should succeed")
8776            .expect("test operation should succeed");
8777        assert_eq!(
8778            features,
8779            ProtocolV2LsRefsFeatures {
8780                unborn: true,
8781                unknown: vec!["custom".into()],
8782            }
8783        );
8784        assert_eq!(
8785            encode_protocol_v2_ls_refs_capability(&features)
8786                .expect("test operation should succeed"),
8787            capabilities[0]
8788        );
8789        assert_eq!(
8790            parse_protocol_v2_ls_refs_features(&[Capability {
8791                name: "ls-refs".into(),
8792                value: None,
8793            }])
8794            .expect("test operation should succeed")
8795            .expect("test operation should succeed"),
8796            ProtocolV2LsRefsFeatures::default()
8797        );
8798        assert!(
8799            parse_protocol_v2_ls_refs_features(&[Capability {
8800                name: "fetch".into(),
8801                value: Some("filter".into()),
8802            }])
8803            .expect("test operation should succeed")
8804            .is_none()
8805        );
8806    }
8807
8808    #[test]
8809    fn protocol_v2_ls_refs_features_reject_malformed_advertisements() {
8810        assert!(
8811            parse_protocol_v2_ls_refs_features(&[
8812                Capability {
8813                    name: "ls-refs".into(),
8814                    value: None,
8815                },
8816                Capability {
8817                    name: "ls-refs".into(),
8818                    value: None,
8819                },
8820            ])
8821            .is_err()
8822        );
8823        assert!(
8824            parse_protocol_v2_ls_refs_features(&[Capability {
8825                name: "ls-refs".into(),
8826                value: Some("unborn  custom".into()),
8827            }])
8828            .is_err()
8829        );
8830        assert!(
8831            encode_protocol_v2_ls_refs_capability(&ProtocolV2LsRefsFeatures {
8832                unknown: vec!["unborn".into()],
8833                ..ProtocolV2LsRefsFeatures::default()
8834            })
8835            .is_err()
8836        );
8837    }
8838
8839    #[test]
8840    fn protocol_v2_ls_refs_command_request_validates_unborn_feature() {
8841        let handshake = TransportHandshake {
8842            protocol: ProtocolVersion::V2,
8843            capabilities: vec![Capability {
8844                name: "ls-refs".into(),
8845                value: Some("unborn".into()),
8846            }],
8847        };
8848        let request = ProtocolV2CommandRequest {
8849            command: "ls-refs".into(),
8850            capabilities: Vec::new(),
8851            arguments: vec![b"unborn".to_vec(), b"ref-prefix HEAD".to_vec()],
8852        };
8853        let parsed = validate_protocol_v2_ls_refs_command_request(&handshake, &request)
8854            .expect("test operation should succeed");
8855        assert!(parsed.unborn);
8856        assert_eq!(parsed.ref_prefixes, vec!["HEAD"]);
8857
8858        let blocked = TransportHandshake {
8859            protocol: ProtocolVersion::V2,
8860            capabilities: vec![Capability {
8861                name: "ls-refs".into(),
8862                value: None,
8863            }],
8864        };
8865        assert!(validate_protocol_v2_ls_refs_command_request(&blocked, &request).is_err());
8866    }
8867
8868    #[test]
8869    fn protocol_v2_fetch_features_parse_and_encode_advertisement() {
8870        let capabilities = vec![Capability {
8871            name: "fetch".into(),
8872            value: Some(
8873                "shallow wait-for-done filter ref-in-want sideband-all packfile-uris custom".into(),
8874            ),
8875        }];
8876        let features = parse_protocol_v2_fetch_features(&capabilities)
8877            .expect("test operation should succeed")
8878            .expect("test operation should succeed");
8879        assert_eq!(
8880            features,
8881            ProtocolV2FetchFeatures {
8882                shallow: true,
8883                wait_for_done: true,
8884                filter: true,
8885                ref_in_want: true,
8886                sideband_all: true,
8887                packfile_uris: true,
8888                unknown: vec!["custom".into()],
8889            }
8890        );
8891        assert_eq!(
8892            encode_protocol_v2_fetch_capability(&features).expect("test operation should succeed"),
8893            capabilities[0]
8894        );
8895        assert_eq!(
8896            parse_protocol_v2_fetch_features(&[Capability {
8897                name: "fetch".into(),
8898                value: None,
8899            }])
8900            .expect("test operation should succeed")
8901            .expect("test operation should succeed"),
8902            ProtocolV2FetchFeatures::default()
8903        );
8904        assert!(
8905            parse_protocol_v2_fetch_features(&[])
8906                .expect("test operation should succeed")
8907                .is_none()
8908        );
8909    }
8910
8911    #[test]
8912    fn protocol_v2_fetch_features_reject_malformed_advertisements() {
8913        assert!(
8914            parse_protocol_v2_fetch_features(&[
8915                Capability {
8916                    name: "fetch".into(),
8917                    value: None,
8918                },
8919                Capability {
8920                    name: "fetch".into(),
8921                    value: None,
8922                },
8923            ])
8924            .is_err()
8925        );
8926        assert!(
8927            parse_protocol_v2_fetch_features(&[Capability {
8928                name: "fetch".into(),
8929                value: Some("filter  shallow".into()),
8930            }])
8931            .is_err()
8932        );
8933        assert!(
8934            encode_protocol_v2_fetch_capability(&ProtocolV2FetchFeatures {
8935                unknown: vec!["filter".into()],
8936                ..ProtocolV2FetchFeatures::default()
8937            })
8938            .is_err()
8939        );
8940    }
8941
8942    #[test]
8943    fn protocol_v2_fetch_request_features_validate_feature_gated_arguments() {
8944        let features = ProtocolV2FetchFeatures {
8945            shallow: true,
8946            wait_for_done: true,
8947            filter: true,
8948            ref_in_want: true,
8949            sideband_all: true,
8950            packfile_uris: true,
8951            unknown: Vec::new(),
8952        };
8953        validate_protocol_v2_fetch_request_features(
8954            &features,
8955            &ProtocolV2FetchRequest {
8956                want_refs: vec!["refs/heads/main".into()],
8957                shallow: vec![
8958                    ObjectId::from_hex(
8959                        ObjectFormat::Sha1,
8960                        "1111111111111111111111111111111111111111",
8961                    )
8962                    .expect("test operation should succeed"),
8963                ],
8964                deepen: Some(1),
8965                filter: Some("blob:none".into()),
8966                packfile_uris: Some("https".into()),
8967                sideband_all: true,
8968                wait_for_done: true,
8969                ..ProtocolV2FetchRequest::default()
8970            },
8971        )
8972        .expect("test operation should succeed");
8973
8974        let request = ProtocolV2FetchRequest {
8975            want_refs: vec!["refs/heads/main".into()],
8976            filter: Some("blob:none".into()),
8977            sideband_all: true,
8978            ..ProtocolV2FetchRequest::default()
8979        };
8980        assert!(
8981            validate_protocol_v2_fetch_request_features(
8982                &ProtocolV2FetchFeatures::default(),
8983                &request,
8984            )
8985            .is_err()
8986        );
8987        assert!(
8988            validate_protocol_v2_fetch_request_features(
8989                &ProtocolV2FetchFeatures {
8990                    ref_in_want: true,
8991                    filter: true,
8992                    ..ProtocolV2FetchFeatures::default()
8993                },
8994                &request,
8995            )
8996            .is_err()
8997        );
8998    }
8999
9000    #[test]
9001    fn protocol_v2_fetch_command_request_validates_against_handshake_features() {
9002        let handshake = TransportHandshake {
9003            protocol: ProtocolVersion::V2,
9004            capabilities: vec![
9005                Capability {
9006                    name: "fetch".into(),
9007                    value: Some("filter ref-in-want".into()),
9008                },
9009                Capability {
9010                    name: "agent".into(),
9011                    value: Some("sley/0".into()),
9012                },
9013            ],
9014        };
9015        let request = ProtocolV2CommandRequest {
9016            command: "fetch".into(),
9017            capabilities: vec![Capability {
9018                name: "agent".into(),
9019                value: Some("client/1".into()),
9020            }],
9021            arguments: vec![
9022                b"want-ref refs/heads/main".to_vec(),
9023                b"filter blob:none".to_vec(),
9024            ],
9025        };
9026        let fetch =
9027            validate_protocol_v2_fetch_command_request(&handshake, ObjectFormat::Sha1, &request)
9028                .expect("test operation should succeed");
9029        assert_eq!(fetch.want_refs, vec!["refs/heads/main"]);
9030        assert_eq!(fetch.filter.as_deref(), Some("blob:none"));
9031
9032        let mut bad = request.clone();
9033        bad.arguments.push(b"sideband-all".to_vec());
9034        assert!(
9035            validate_protocol_v2_fetch_command_request(&handshake, ObjectFormat::Sha1, &bad)
9036                .is_err()
9037        );
9038    }
9039
9040    #[test]
9041    fn protocol_v2_object_info_request_parses_encodes_and_validates() {
9042        let oid = ObjectId::from_hex(
9043            ObjectFormat::Sha1,
9044            "1111111111111111111111111111111111111111",
9045        )
9046        .expect("test operation should succeed");
9047        let request = ProtocolV2CommandRequest {
9048            command: "object-info".into(),
9049            capabilities: Vec::new(),
9050            arguments: vec![
9051                b"size".to_vec(),
9052                b"oid 1111111111111111111111111111111111111111".to_vec(),
9053            ],
9054        };
9055        let parsed =
9056            ProtocolV2ObjectInfoRequest::from_command_request(ObjectFormat::Sha1, &request)
9057                .expect("test operation should succeed");
9058        assert_eq!(
9059            parsed,
9060            ProtocolV2ObjectInfoRequest {
9061                size: true,
9062                oids: vec![oid],
9063            }
9064        );
9065        assert_eq!(
9066            parsed
9067                .to_command_request()
9068                .expect("test operation should succeed"),
9069            request
9070        );
9071
9072        let handshake = TransportHandshake {
9073            protocol: ProtocolVersion::V2,
9074            capabilities: vec![Capability {
9075                name: "object-info".into(),
9076                value: None,
9077            }],
9078        };
9079        assert_eq!(
9080            validate_protocol_v2_object_info_command_request(
9081                &handshake,
9082                ObjectFormat::Sha1,
9083                &request,
9084            )
9085            .expect("test operation should succeed"),
9086            parsed
9087        );
9088    }
9089
9090    #[test]
9091    fn protocol_v2_object_info_request_streams_round_trip() {
9092        let request = ProtocolV2ObjectInfoRequest {
9093            size: true,
9094            oids: vec![
9095                ObjectId::from_hex(
9096                    ObjectFormat::Sha1,
9097                    "1111111111111111111111111111111111111111",
9098                )
9099                .expect("test operation should succeed"),
9100            ],
9101        };
9102        let mut encoded = Vec::new();
9103        write_protocol_v2_object_info_request(&mut encoded, &request)
9104            .expect("test operation should succeed");
9105        encoded.extend_from_slice(b"tail");
9106
9107        let mut input = encoded.as_slice();
9108        assert_eq!(
9109            read_protocol_v2_object_info_request(ObjectFormat::Sha1, &mut input)
9110                .expect("test operation should succeed"),
9111            request
9112        );
9113        assert_eq!(input, b"tail");
9114    }
9115
9116    #[test]
9117    fn protocol_v2_object_info_request_rejects_malformed_arguments() {
9118        assert!(
9119            ProtocolV2ObjectInfoRequest::from_command_request(
9120                ObjectFormat::Sha1,
9121                &ProtocolV2CommandRequest {
9122                    command: "object-info".into(),
9123                    capabilities: Vec::new(),
9124                    arguments: vec![b"oid 1111111111111111111111111111111111111111".to_vec()],
9125                },
9126            )
9127            .is_err()
9128        );
9129        assert!(
9130            ProtocolV2ObjectInfoRequest::from_command_request(
9131                ObjectFormat::Sha1,
9132                &ProtocolV2CommandRequest {
9133                    command: "object-info".into(),
9134                    capabilities: Vec::new(),
9135                    arguments: vec![b"size".to_vec(), b"size".to_vec()],
9136                },
9137            )
9138            .is_err()
9139        );
9140        assert!(
9141            ProtocolV2ObjectInfoRequest::from_command_request(
9142                ObjectFormat::Sha1,
9143                &ProtocolV2CommandRequest {
9144                    command: "object-info".into(),
9145                    capabilities: Vec::new(),
9146                    arguments: vec![b"size".to_vec()],
9147                },
9148            )
9149            .is_err()
9150        );
9151        assert!(
9152            ProtocolV2ObjectInfoRequest::from_command_request(
9153                ObjectFormat::Sha1,
9154                &ProtocolV2CommandRequest {
9155                    command: "object-info".into(),
9156                    capabilities: Vec::new(),
9157                    arguments: vec![b"size".to_vec(), b"oid not-an-oid".to_vec()],
9158                },
9159            )
9160            .is_err()
9161        );
9162        assert!(
9163            validate_protocol_v2_object_info_command_request(
9164                &TransportHandshake {
9165                    protocol: ProtocolVersion::V2,
9166                    capabilities: Vec::new(),
9167                },
9168                ObjectFormat::Sha1,
9169                &ProtocolV2CommandRequest {
9170                    command: "object-info".into(),
9171                    capabilities: Vec::new(),
9172                    arguments: vec![
9173                        b"size".to_vec(),
9174                        b"oid 1111111111111111111111111111111111111111".to_vec(),
9175                    ],
9176                },
9177            )
9178            .is_err()
9179        );
9180    }
9181
9182    #[test]
9183    fn protocol_v2_command_request_classifies_known_and_unknown_commands() {
9184        let handshake = TransportHandshake {
9185            protocol: ProtocolVersion::V2,
9186            capabilities: vec![
9187                Capability {
9188                    name: "ls-refs".into(),
9189                    value: Some("unborn".into()),
9190                },
9191                Capability {
9192                    name: "fetch".into(),
9193                    value: Some("filter ref-in-want".into()),
9194                },
9195                Capability {
9196                    name: "object-info".into(),
9197                    value: None,
9198                },
9199                Capability {
9200                    name: "server-option".into(),
9201                    value: None,
9202                },
9203                Capability {
9204                    name: "server-info".into(),
9205                    value: Some("custom".into()),
9206                },
9207            ],
9208        };
9209        assert_eq!(
9210            classify_protocol_v2_command_request(
9211                &handshake,
9212                ObjectFormat::Sha1,
9213                &ProtocolV2CommandRequest {
9214                    command: "ls-refs".into(),
9215                    capabilities: Vec::new(),
9216                    arguments: vec![b"unborn".to_vec()],
9217                },
9218            )
9219            .expect("test operation should succeed"),
9220            ProtocolV2Command::LsRefs(ProtocolV2LsRefsRequest {
9221                unborn: true,
9222                ..ProtocolV2LsRefsRequest::default()
9223            })
9224        );
9225        assert_eq!(
9226            classify_protocol_v2_command_request(
9227                &handshake,
9228                ObjectFormat::Sha1,
9229                &ProtocolV2CommandRequest {
9230                    command: "fetch".into(),
9231                    capabilities: Vec::new(),
9232                    arguments: vec![
9233                        b"want-ref refs/heads/main".to_vec(),
9234                        b"filter blob:none".to_vec(),
9235                    ],
9236                },
9237            )
9238            .expect("test operation should succeed"),
9239            ProtocolV2Command::Fetch(ProtocolV2FetchRequest {
9240                want_refs: vec!["refs/heads/main".into()],
9241                filter: Some("blob:none".into()),
9242                ..ProtocolV2FetchRequest::default()
9243            })
9244        );
9245        assert_eq!(
9246            classify_protocol_v2_command_request(
9247                &handshake,
9248                ObjectFormat::Sha1,
9249                &ProtocolV2CommandRequest {
9250                    command: "object-info".into(),
9251                    capabilities: Vec::new(),
9252                    arguments: vec![
9253                        b"size".to_vec(),
9254                        b"oid 1111111111111111111111111111111111111111".to_vec(),
9255                    ],
9256                },
9257            )
9258            .expect("test operation should succeed"),
9259            ProtocolV2Command::ObjectInfo(ProtocolV2ObjectInfoRequest {
9260                size: true,
9261                oids: vec![
9262                    ObjectId::from_hex(
9263                        ObjectFormat::Sha1,
9264                        "1111111111111111111111111111111111111111",
9265                    )
9266                    .expect("test operation should succeed")
9267                ],
9268            })
9269        );
9270
9271        let unknown = ProtocolV2CommandRequest {
9272            command: "server-info".into(),
9273            capabilities: vec![Capability {
9274                name: "server-option".into(),
9275                value: Some("trace=true".into()),
9276            }],
9277            arguments: Vec::new(),
9278        };
9279        assert_eq!(
9280            classify_protocol_v2_command_request(&handshake, ObjectFormat::Sha1, &unknown)
9281                .expect("test operation should succeed"),
9282            ProtocolV2Command::Unknown(unknown)
9283        );
9284        assert!(
9285            classify_protocol_v2_command_request(
9286                &handshake,
9287                ObjectFormat::Sha1,
9288                &ProtocolV2CommandRequest {
9289                    command: "not-advertised".into(),
9290                    capabilities: Vec::new(),
9291                    arguments: Vec::new(),
9292                },
9293            )
9294            .is_err()
9295        );
9296    }
9297
9298    #[test]
9299    fn protocol_v2_session_request_classifies_streamed_command_and_done() {
9300        let handshake = TransportHandshake {
9301            protocol: ProtocolVersion::V2,
9302            capabilities: vec![
9303                Capability {
9304                    name: "ls-refs".into(),
9305                    value: Some("unborn".into()),
9306                },
9307                Capability {
9308                    name: "fetch".into(),
9309                    value: Some("filter ref-in-want".into()),
9310                },
9311            ],
9312        };
9313        let command = ProtocolV2Request::Command(ProtocolV2CommandRequest {
9314            command: "ls-refs".into(),
9315            capabilities: Vec::new(),
9316            arguments: vec![b"unborn".to_vec()],
9317        });
9318        assert_eq!(
9319            classify_protocol_v2_request(&handshake, ObjectFormat::Sha1, &command)
9320                .expect("test operation should succeed"),
9321            ProtocolV2SessionRequest::Command(ProtocolV2Command::LsRefs(ProtocolV2LsRefsRequest {
9322                unborn: true,
9323                ..ProtocolV2LsRefsRequest::default()
9324            }))
9325        );
9326        assert_eq!(
9327            classify_protocol_v2_request(&handshake, ObjectFormat::Sha1, &ProtocolV2Request::Done)
9328                .expect("test operation should succeed"),
9329            ProtocolV2SessionRequest::Done
9330        );
9331
9332        let mut encoded = Vec::new();
9333        write_protocol_v2_request(&mut encoded, &command).expect("test operation should succeed");
9334        write_protocol_v2_request(&mut encoded, &ProtocolV2Request::Done)
9335            .expect("test operation should succeed");
9336        encoded.extend_from_slice(b"tail");
9337
9338        let mut input = encoded.as_slice();
9339        assert_eq!(
9340            read_protocol_v2_session_request(&handshake, ObjectFormat::Sha1, &mut input)
9341                .expect("test operation should succeed"),
9342            ProtocolV2SessionRequest::Command(ProtocolV2Command::LsRefs(ProtocolV2LsRefsRequest {
9343                unborn: true,
9344                ..ProtocolV2LsRefsRequest::default()
9345            }))
9346        );
9347        assert_eq!(
9348            read_protocol_v2_session_request(&handshake, ObjectFormat::Sha1, &mut input)
9349                .expect("test operation should succeed"),
9350            ProtocolV2SessionRequest::Done
9351        );
9352        assert_eq!(input, b"tail");
9353    }
9354
9355    #[test]
9356    fn advertised_ref_parses_first_v0_capability_line() {
9357        let payload =
9358            b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 HEAD\0multi_ack symref=HEAD:refs/heads/main\n";
9359        let advertisement = parse_ref_advertisement(ObjectFormat::Sha1, payload)
9360            .expect("test operation should succeed");
9361        assert_eq!(
9362            advertisement.oid,
9363            ObjectId::from_hex(
9364                ObjectFormat::Sha1,
9365                "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"
9366            )
9367            .expect("test operation should succeed")
9368        );
9369        assert_eq!(advertisement.name, "HEAD");
9370        assert_eq!(
9371            advertisement.capabilities,
9372            vec![
9373                Capability {
9374                    name: "multi_ack".into(),
9375                    value: None,
9376                },
9377                Capability {
9378                    name: "symref".into(),
9379                    value: Some("HEAD:refs/heads/main".into()),
9380                },
9381            ]
9382        );
9383    }
9384
9385    #[test]
9386    fn advertised_ref_parses_lines_without_capabilities() {
9387        let advertisement = parse_ref_advertisement(
9388            ObjectFormat::Sha1,
9389            b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n",
9390        )
9391        .expect("test operation should succeed");
9392        assert_eq!(advertisement.name, "refs/heads/main");
9393        assert!(advertisement.capabilities.is_empty());
9394    }
9395
9396    #[test]
9397    fn advertised_ref_rejects_malformed_payloads() {
9398        assert!(
9399            parse_ref_advertisement(ObjectFormat::Sha1, b"not-an-oid refs/heads/main\n").is_err()
9400        );
9401        assert!(
9402            parse_ref_advertisement(
9403                ObjectFormat::Sha1,
9404                b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n"
9405            )
9406            .is_err()
9407        );
9408    }
9409
9410    #[test]
9411    fn advertised_refs_parse_and_encode_stream() {
9412        let main = ObjectId::from_hex(
9413            ObjectFormat::Sha1,
9414            "1111111111111111111111111111111111111111",
9415        )
9416        .expect("test operation should succeed");
9417        let feature = ObjectId::from_hex(
9418            ObjectFormat::Sha1,
9419            "2222222222222222222222222222222222222222",
9420        )
9421        .expect("test operation should succeed");
9422        let frames = vec![
9423            PktLineFrame::Data(
9424                b"1111111111111111111111111111111111111111 HEAD\0multi_ack thin-pack agent=git/2.54.0\n"
9425                    .to_vec(),
9426            ),
9427            PktLineFrame::Data(
9428                b"2222222222222222222222222222222222222222 refs/heads/feature\n".to_vec(),
9429            ),
9430            PktLineFrame::Flush,
9431        ];
9432        let advertisements = parse_ref_advertisements(ObjectFormat::Sha1, &frames)
9433            .expect("test operation should succeed");
9434        assert_eq!(
9435            advertisements,
9436            vec![
9437                RefAdvertisement {
9438                    oid: main,
9439                    name: "HEAD".into(),
9440                    capabilities: vec![
9441                        Capability {
9442                            name: "multi_ack".into(),
9443                            value: None,
9444                        },
9445                        Capability {
9446                            name: "thin-pack".into(),
9447                            value: None,
9448                        },
9449                        Capability {
9450                            name: "agent".into(),
9451                            value: Some("git/2.54.0".into()),
9452                        },
9453                    ],
9454                },
9455                RefAdvertisement {
9456                    oid: feature,
9457                    name: "refs/heads/feature".into(),
9458                    capabilities: Vec::new(),
9459                },
9460            ]
9461        );
9462        assert_eq!(
9463            encode_ref_advertisements(&advertisements).expect("test operation should succeed"),
9464            frames
9465        );
9466        assert_eq!(
9467            parse_ref_advertisements(ObjectFormat::Sha1, &[PktLineFrame::Flush])
9468                .expect("test operation should succeed"),
9469            Vec::<RefAdvertisement>::new()
9470        );
9471    }
9472
9473    #[test]
9474    fn advertised_ref_set_parses_v1_version_refs_and_shallow() {
9475        let main = ObjectId::from_hex(
9476            ObjectFormat::Sha1,
9477            "1111111111111111111111111111111111111111",
9478        )
9479        .expect("test operation should succeed");
9480        let feature = ObjectId::from_hex(
9481            ObjectFormat::Sha1,
9482            "2222222222222222222222222222222222222222",
9483        )
9484        .expect("test operation should succeed");
9485        let shallow = ObjectId::from_hex(
9486            ObjectFormat::Sha1,
9487            "3333333333333333333333333333333333333333",
9488        )
9489        .expect("test operation should succeed");
9490        let frames = vec![
9491            PktLineFrame::Data(b"version 1\n".to_vec()),
9492            PktLineFrame::Data(
9493                b"1111111111111111111111111111111111111111 HEAD\0multi_ack symref=HEAD:refs/heads/main\n"
9494                    .to_vec(),
9495            ),
9496            PktLineFrame::Data(
9497                b"2222222222222222222222222222222222222222 refs/heads/feature\n".to_vec(),
9498            ),
9499            PktLineFrame::Data(b"shallow 3333333333333333333333333333333333333333\n".to_vec()),
9500            PktLineFrame::Flush,
9501        ];
9502
9503        let set = parse_ref_advertisement_set(ObjectFormat::Sha1, &frames)
9504            .expect("test operation should succeed");
9505        assert_eq!(set.protocol, ProtocolVersion::V1);
9506        assert_eq!(set.shallow, vec![shallow]);
9507        assert_eq!(
9508            set.refs,
9509            vec![
9510                RefAdvertisement {
9511                    oid: main,
9512                    name: "HEAD".into(),
9513                    capabilities: vec![
9514                        Capability {
9515                            name: "multi_ack".into(),
9516                            value: None,
9517                        },
9518                        Capability {
9519                            name: "symref".into(),
9520                            value: Some("HEAD:refs/heads/main".into()),
9521                        },
9522                    ],
9523                },
9524                RefAdvertisement {
9525                    oid: feature,
9526                    name: "refs/heads/feature".into(),
9527                    capabilities: Vec::new(),
9528                },
9529            ]
9530        );
9531        assert_eq!(
9532            parse_ref_advertisements(ObjectFormat::Sha1, &frames)
9533                .expect("test operation should succeed"),
9534            set.refs
9535        );
9536        assert_eq!(
9537            encode_ref_advertisement_set(&set).expect("test operation should succeed"),
9538            frames
9539        );
9540    }
9541
9542    #[test]
9543    fn advertised_refs_streams_round_trip() {
9544        let advertisements = vec![RefAdvertisement {
9545            oid: ObjectId::from_hex(
9546                ObjectFormat::Sha1,
9547                "1111111111111111111111111111111111111111",
9548            )
9549            .expect("test operation should succeed"),
9550            name: "HEAD".into(),
9551            capabilities: vec![Capability {
9552                name: "symref".into(),
9553                value: Some("HEAD:refs/heads/main".into()),
9554            }],
9555        }];
9556        let mut encoded = Vec::new();
9557        write_ref_advertisements(&mut encoded, &advertisements)
9558            .expect("test operation should succeed");
9559        encoded.extend_from_slice(b"tail");
9560
9561        let mut input = encoded.as_slice();
9562        assert_eq!(
9563            read_ref_advertisements(ObjectFormat::Sha1, &mut input)
9564                .expect("test operation should succeed"),
9565            advertisements
9566        );
9567        assert_eq!(input, b"tail");
9568    }
9569
9570    #[test]
9571    fn advertised_ref_set_streams_round_trip() {
9572        let set = RefAdvertisementSet {
9573            protocol: ProtocolVersion::V1,
9574            refs: vec![RefAdvertisement {
9575                oid: ObjectId::from_hex(
9576                    ObjectFormat::Sha1,
9577                    "1111111111111111111111111111111111111111",
9578                )
9579                .expect("test operation should succeed"),
9580                name: "HEAD".into(),
9581                capabilities: vec![Capability {
9582                    name: "symref".into(),
9583                    value: Some("HEAD:refs/heads/main".into()),
9584                }],
9585            }],
9586            shallow: vec![
9587                ObjectId::from_hex(
9588                    ObjectFormat::Sha1,
9589                    "2222222222222222222222222222222222222222",
9590                )
9591                .expect("test operation should succeed"),
9592            ],
9593        };
9594        let mut encoded = Vec::new();
9595        write_ref_advertisement_set(&mut encoded, &set).expect("test operation should succeed");
9596        encoded.extend_from_slice(b"tail");
9597
9598        let mut input = encoded.as_slice();
9599        assert_eq!(
9600            read_ref_advertisement_set(ObjectFormat::Sha1, &mut input)
9601                .expect("test operation should succeed"),
9602            set
9603        );
9604        assert_eq!(input, b"tail");
9605    }
9606
9607    #[test]
9608    fn advertised_refs_reject_malformed_streams() {
9609        assert!(
9610            parse_ref_advertisements(
9611                ObjectFormat::Sha1,
9612                &[PktLineFrame::Data(
9613                    b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),
9614                )],
9615            )
9616            .is_err()
9617        );
9618        assert!(
9619            parse_ref_advertisements(
9620                ObjectFormat::Sha1,
9621                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
9622            )
9623            .is_err()
9624        );
9625        assert!(parse_ref_advertisements(
9626            ObjectFormat::Sha1,
9627            &[
9628                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
9629                PktLineFrame::Data(
9630                    b"2222222222222222222222222222222222222222 refs/heads/main\0thin-pack\n"
9631                        .to_vec(),
9632                ),
9633                PktLineFrame::Flush,
9634            ],
9635        )
9636        .is_err());
9637        assert!(parse_ref_advertisement_set(
9638            ObjectFormat::Sha1,
9639            &[
9640                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
9641                PktLineFrame::Data(b"version 1\n".to_vec()),
9642                PktLineFrame::Flush,
9643            ],
9644        )
9645        .is_err());
9646        assert!(
9647            parse_ref_advertisement_set(
9648                ObjectFormat::Sha1,
9649                &[
9650                    PktLineFrame::Data(b"version 2\n".to_vec()),
9651                    PktLineFrame::Flush,
9652                ],
9653            )
9654            .is_err()
9655        );
9656        assert!(
9657            parse_ref_advertisement_set(
9658                ObjectFormat::Sha1,
9659                &[
9660                    PktLineFrame::Data(
9661                        b"shallow 1111111111111111111111111111111111111111\n".to_vec()
9662                    ),
9663                    PktLineFrame::Flush,
9664                ],
9665            )
9666            .is_err()
9667        );
9668        assert!(parse_ref_advertisement_set(
9669            ObjectFormat::Sha1,
9670            &[
9671                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
9672                PktLineFrame::Data(b"shallow not-an-oid\n".to_vec()),
9673                PktLineFrame::Flush,
9674            ],
9675        )
9676        .is_err());
9677        assert!(parse_ref_advertisement_set(
9678            ObjectFormat::Sha1,
9679            &[
9680                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
9681                PktLineFrame::Data(b"shallow 2222222222222222222222222222222222222222\n".to_vec()),
9682                PktLineFrame::Data(
9683                    b"3333333333333333333333333333333333333333 refs/heads/main\n".to_vec(),
9684                ),
9685                PktLineFrame::Flush,
9686            ],
9687        )
9688        .is_err());
9689        assert!(
9690            encode_ref_advertisements(&[
9691                RefAdvertisement {
9692                    oid: ObjectId::from_hex(
9693                        ObjectFormat::Sha1,
9694                        "1111111111111111111111111111111111111111",
9695                    )
9696                    .expect("test operation should succeed"),
9697                    name: "HEAD".into(),
9698                    capabilities: Vec::new(),
9699                },
9700                RefAdvertisement {
9701                    oid: ObjectId::from_hex(
9702                        ObjectFormat::Sha1,
9703                        "2222222222222222222222222222222222222222",
9704                    )
9705                    .expect("test operation should succeed"),
9706                    name: "refs/heads/main".into(),
9707                    capabilities: vec![Capability {
9708                        name: "thin-pack".into(),
9709                        value: None,
9710                    }],
9711                },
9712            ])
9713            .is_err()
9714        );
9715        assert!(
9716            encode_ref_advertisement(&RefAdvertisement {
9717                oid: ObjectId::from_hex(
9718                    ObjectFormat::Sha1,
9719                    "1111111111111111111111111111111111111111",
9720                )
9721                .expect("test operation should succeed"),
9722                name: "bad ref".into(),
9723                capabilities: Vec::new(),
9724            })
9725            .is_err()
9726        );
9727        assert!(
9728            encode_ref_advertisement_set(&RefAdvertisementSet {
9729                protocol: ProtocolVersion::V2,
9730                refs: Vec::new(),
9731                shallow: Vec::new(),
9732            })
9733            .is_err()
9734        );
9735        assert!(
9736            encode_ref_advertisement_set(&RefAdvertisementSet {
9737                protocol: ProtocolVersion::V0,
9738                refs: Vec::new(),
9739                shallow: vec![
9740                    ObjectId::from_hex(
9741                        ObjectFormat::Sha1,
9742                        "1111111111111111111111111111111111111111",
9743                    )
9744                    .expect("test operation should succeed")
9745                ],
9746            })
9747            .is_err()
9748        );
9749    }
9750
9751    #[test]
9752    fn dumb_http_info_refs_parse_and_encode_records() {
9753        let main = ObjectId::from_hex(
9754            ObjectFormat::Sha1,
9755            "1111111111111111111111111111111111111111",
9756        )
9757        .expect("test operation should succeed");
9758        let tag = ObjectId::from_hex(
9759            ObjectFormat::Sha1,
9760            "2222222222222222222222222222222222222222",
9761        )
9762        .expect("test operation should succeed");
9763        let peeled = ObjectId::from_hex(
9764            ObjectFormat::Sha1,
9765            "3333333333333333333333333333333333333333",
9766        )
9767        .expect("test operation should succeed");
9768        let input = b"1111111111111111111111111111111111111111\trefs/heads/main\n2222222222222222222222222222222222222222\trefs/tags/v1.0\n3333333333333333333333333333333333333333\trefs/tags/v1.0^{}\n";
9769
9770        let records = parse_dumb_http_info_refs(ObjectFormat::Sha1, input)
9771            .expect("test operation should succeed");
9772        assert_eq!(
9773            records,
9774            vec![
9775                DumbHttpRefRecord {
9776                    oid: main,
9777                    name: "refs/heads/main".into(),
9778                    peeled: false,
9779                },
9780                DumbHttpRefRecord {
9781                    oid: tag,
9782                    name: "refs/tags/v1.0".into(),
9783                    peeled: false,
9784                },
9785                DumbHttpRefRecord {
9786                    oid: peeled,
9787                    name: "refs/tags/v1.0".into(),
9788                    peeled: true,
9789                },
9790            ]
9791        );
9792        assert_eq!(
9793            encode_dumb_http_info_refs(&records).expect("test operation should succeed"),
9794            input
9795        );
9796        assert_eq!(
9797            parse_dumb_http_info_refs(ObjectFormat::Sha1, b"")
9798                .expect("test operation should succeed"),
9799            Vec::<DumbHttpRefRecord>::new()
9800        );
9801    }
9802
9803    #[test]
9804    fn dumb_http_info_refs_streams_round_trip() {
9805        let records = vec![DumbHttpRefRecord {
9806            oid: ObjectId::from_hex(
9807                ObjectFormat::Sha1,
9808                "1111111111111111111111111111111111111111",
9809            )
9810            .expect("test operation should succeed"),
9811            name: "refs/heads/main".into(),
9812            peeled: false,
9813        }];
9814        let mut encoded = Vec::new();
9815        write_dumb_http_info_refs(&mut encoded, &records).expect("test operation should succeed");
9816        let mut input = encoded.as_slice();
9817        assert_eq!(
9818            read_dumb_http_info_refs(ObjectFormat::Sha1, &mut input)
9819                .expect("test operation should succeed"),
9820            records
9821        );
9822        assert!(input.is_empty());
9823    }
9824
9825    #[test]
9826    fn dumb_http_info_refs_reject_malformed_records() {
9827        assert!(
9828            parse_dumb_http_info_refs(
9829                ObjectFormat::Sha1,
9830                b"1111111111111111111111111111111111111111 refs/heads/main\n",
9831            )
9832            .is_err()
9833        );
9834        assert!(
9835            parse_dumb_http_info_refs(
9836                ObjectFormat::Sha1,
9837                b"1111111111111111111111111111111111111111\trefs/heads/main",
9838            )
9839            .is_err()
9840        );
9841        assert!(
9842            parse_dumb_http_info_refs(ObjectFormat::Sha1, b"not-an-oid\trefs/heads/main\n")
9843                .is_err()
9844        );
9845        assert!(
9846            parse_dumb_http_info_refs(
9847                ObjectFormat::Sha1,
9848                b"1111111111111111111111111111111111111111\tbad ref\n",
9849            )
9850            .is_err()
9851        );
9852        assert!(
9853            encode_dumb_http_info_refs(&[DumbHttpRefRecord {
9854                oid: ObjectId::from_hex(
9855                    ObjectFormat::Sha1,
9856                    "1111111111111111111111111111111111111111",
9857                )
9858                .expect("test operation should succeed"),
9859                name: "refs/tags/v1.0^{}".into(),
9860                peeled: false,
9861            }])
9862            .is_err()
9863        );
9864    }
9865
9866    #[test]
9867    fn dumb_http_alternates_parse_and_encode_locations() {
9868        let input = b"https://example.com/base.git/objects/\n../other.git/objects/\n";
9869        let alternates = parse_dumb_http_alternates(input).expect("test operation should succeed");
9870        assert_eq!(
9871            alternates,
9872            vec![
9873                "https://example.com/base.git/objects/".to_string(),
9874                "../other.git/objects/".to_string(),
9875            ]
9876        );
9877        assert_eq!(
9878            encode_dumb_http_alternates(&alternates).expect("test operation should succeed"),
9879            input
9880        );
9881        assert_eq!(
9882            parse_dumb_http_alternates(b"").expect("test operation should succeed"),
9883            Vec::<String>::new()
9884        );
9885    }
9886
9887    #[test]
9888    fn dumb_http_alternates_streams_round_trip() {
9889        let alternates = vec!["https://example.com/base.git/objects/".to_string()];
9890        let mut encoded = Vec::new();
9891        write_dumb_http_alternates(&mut encoded, &alternates)
9892            .expect("test operation should succeed");
9893        let mut input = encoded.as_slice();
9894        assert_eq!(
9895            read_dumb_http_alternates(&mut input).expect("test operation should succeed"),
9896            alternates
9897        );
9898        assert!(input.is_empty());
9899    }
9900
9901    #[test]
9902    fn dumb_http_alternates_reject_malformed_lines() {
9903        assert!(parse_dumb_http_alternates(b"https://example.com/base.git/objects/").is_err());
9904        assert!(parse_dumb_http_alternates(b"\n").is_err());
9905        assert!(parse_dumb_http_alternates(b"https://example.com/base.git/objects/\r\n").is_err());
9906        assert!(encode_dumb_http_alternates(&["bad\nalternate".to_string()]).is_err());
9907    }
9908
9909    #[test]
9910    fn dumb_http_packs_parse_and_encode_pack_records() {
9911        let first = ObjectId::from_hex(
9912            ObjectFormat::Sha1,
9913            "1111111111111111111111111111111111111111",
9914        )
9915        .expect("test operation should succeed");
9916        let second = ObjectId::from_hex(
9917            ObjectFormat::Sha1,
9918            "2222222222222222222222222222222222222222",
9919        )
9920        .expect("test operation should succeed");
9921        let input = b"P pack-1111111111111111111111111111111111111111.pack\nP pack-2222222222222222222222222222222222222222.pack\n";
9922        let records = parse_dumb_http_packs(ObjectFormat::Sha1, input)
9923            .expect("test operation should succeed");
9924        assert_eq!(
9925            records,
9926            vec![
9927                DumbHttpPackRecord { hash: first },
9928                DumbHttpPackRecord { hash: second },
9929            ]
9930        );
9931        assert_eq!(
9932            encode_dumb_http_packs(&records).expect("test operation should succeed"),
9933            input
9934        );
9935        assert_eq!(
9936            parse_dumb_http_packs(ObjectFormat::Sha1, b"").expect("test operation should succeed"),
9937            Vec::<DumbHttpPackRecord>::new()
9938        );
9939    }
9940
9941    #[test]
9942    fn dumb_http_packs_streams_round_trip() {
9943        let records = vec![DumbHttpPackRecord {
9944            hash: ObjectId::from_hex(
9945                ObjectFormat::Sha1,
9946                "1111111111111111111111111111111111111111",
9947            )
9948            .expect("test operation should succeed"),
9949        }];
9950        let mut encoded = Vec::new();
9951        write_dumb_http_packs(&mut encoded, &records).expect("test operation should succeed");
9952        let mut input = encoded.as_slice();
9953        assert_eq!(
9954            read_dumb_http_packs(ObjectFormat::Sha1, &mut input)
9955                .expect("test operation should succeed"),
9956            records
9957        );
9958        assert!(input.is_empty());
9959    }
9960
9961    #[test]
9962    fn dumb_http_packs_reject_malformed_records() {
9963        assert!(
9964            parse_dumb_http_packs(
9965                ObjectFormat::Sha1,
9966                b"P pack-1111111111111111111111111111111111111111.pack",
9967            )
9968            .is_err()
9969        );
9970        assert!(
9971            parse_dumb_http_packs(
9972                ObjectFormat::Sha1,
9973                b"pack-1111111111111111111111111111111111111111.pack\n",
9974            )
9975            .is_err()
9976        );
9977        assert!(parse_dumb_http_packs(ObjectFormat::Sha1, b"P pack-not-a-hash.pack\n",).is_err());
9978        assert!(
9979            parse_dumb_http_packs(
9980                ObjectFormat::Sha1,
9981                b"P pack-1111111111111111111111111111111111111111.idx\n",
9982            )
9983            .is_err()
9984        );
9985    }
9986
9987    #[test]
9988    fn upload_pack_features_parse_encode_and_validate_request() {
9989        let capabilities = vec![
9990            Capability {
9991                name: "multi_ack".into(),
9992                value: None,
9993            },
9994            Capability {
9995                name: "multi_ack_detailed".into(),
9996                value: None,
9997            },
9998            Capability {
9999                name: "no-done".into(),
10000                value: None,
10001            },
10002            Capability {
10003                name: "thin-pack".into(),
10004                value: None,
10005            },
10006            Capability {
10007                name: "side-band-64k".into(),
10008                value: None,
10009            },
10010            Capability {
10011                name: "ofs-delta".into(),
10012                value: None,
10013            },
10014            Capability {
10015                name: "shallow".into(),
10016                value: None,
10017            },
10018            Capability {
10019                name: "deepen-since".into(),
10020                value: None,
10021            },
10022            Capability {
10023                name: "deepen-not".into(),
10024                value: None,
10025            },
10026            Capability {
10027                name: "include-tag".into(),
10028                value: None,
10029            },
10030            Capability {
10031                name: "no-progress".into(),
10032                value: None,
10033            },
10034            Capability {
10035                name: "filter".into(),
10036                value: None,
10037            },
10038            Capability {
10039                name: "agent".into(),
10040                value: Some("git/2.54.0".into()),
10041            },
10042            Capability {
10043                name: "object-format".into(),
10044                value: Some("sha256".into()),
10045            },
10046            Capability {
10047                name: "symref".into(),
10048                value: Some("HEAD:refs/heads/main".into()),
10049            },
10050            Capability {
10051                name: "custom".into(),
10052                value: Some("value".into()),
10053            },
10054        ];
10055        let features =
10056            parse_upload_pack_features(&capabilities).expect("test operation should succeed");
10057        assert_eq!(
10058            features,
10059            UploadPackFeatures {
10060                multi_ack: true,
10061                multi_ack_detailed: true,
10062                no_done: true,
10063                thin_pack: true,
10064                side_band_64k: true,
10065                ofs_delta: true,
10066                shallow: true,
10067                deepen_since: true,
10068                deepen_not: true,
10069                include_tag: true,
10070                no_progress: true,
10071                filter: true,
10072                agent: Some("git/2.54.0".into()),
10073                object_format: Some(ObjectFormat::Sha256),
10074                symrefs: vec!["HEAD:refs/heads/main".into()],
10075                unknown: vec![Capability {
10076                    name: "custom".into(),
10077                    value: Some("value".into()),
10078                }],
10079                ..UploadPackFeatures::default()
10080            }
10081        );
10082        assert_eq!(
10083            encode_upload_pack_features(&features).expect("test operation should succeed"),
10084            capabilities
10085        );
10086
10087        let request = UploadPackRequest {
10088            wants: vec![
10089                ObjectId::from_hex(
10090                    ObjectFormat::Sha1,
10091                    "1111111111111111111111111111111111111111",
10092                )
10093                .expect("test operation should succeed"),
10094            ],
10095            capabilities: vec![
10096                Capability {
10097                    name: "multi_ack_detailed".into(),
10098                    value: None,
10099                },
10100                Capability {
10101                    name: "thin-pack".into(),
10102                    value: None,
10103                },
10104                Capability {
10105                    name: "side-band-64k".into(),
10106                    value: None,
10107                },
10108                Capability {
10109                    name: "ofs-delta".into(),
10110                    value: None,
10111                },
10112                Capability {
10113                    name: "include-tag".into(),
10114                    value: None,
10115                },
10116                Capability {
10117                    name: "agent".into(),
10118                    value: Some("sley".into()),
10119                },
10120            ],
10121            shallow: vec![
10122                ObjectId::from_hex(
10123                    ObjectFormat::Sha1,
10124                    "2222222222222222222222222222222222222222",
10125                )
10126                .expect("test operation should succeed"),
10127            ],
10128            deepen: Some(5),
10129            deepen_since: Some(1_710_000_000),
10130            deepen_not: vec!["refs/tags/base".into()],
10131            filter: Some("blob:none".into()),
10132        };
10133        validate_upload_pack_request_features(&features, &request)
10134            .expect("test operation should succeed");
10135    }
10136
10137    #[test]
10138    fn upload_pack_features_reject_invalid_requests() {
10139        let want = ObjectId::from_hex(
10140            ObjectFormat::Sha1,
10141            "1111111111111111111111111111111111111111",
10142        )
10143        .expect("test operation should succeed");
10144        let features = UploadPackFeatures {
10145            thin_pack: true,
10146            side_band: true,
10147            ..UploadPackFeatures::default()
10148        };
10149
10150        assert!(
10151            validate_upload_pack_request_features(
10152                &features,
10153                &UploadPackRequest {
10154                    wants: vec![want],
10155                    capabilities: vec![Capability {
10156                        name: "ofs-delta".into(),
10157                        value: None,
10158                    }],
10159                    ..UploadPackRequest::default()
10160                },
10161            )
10162            .is_err()
10163        );
10164        assert!(
10165            validate_upload_pack_request_features(
10166                &features,
10167                &UploadPackRequest {
10168                    wants: vec![want],
10169                    shallow: vec![want],
10170                    ..UploadPackRequest::default()
10171                },
10172            )
10173            .is_err()
10174        );
10175        assert!(
10176            validate_upload_pack_request_features(
10177                &features,
10178                &UploadPackRequest {
10179                    wants: vec![want],
10180                    filter: Some("blob:none".into()),
10181                    ..UploadPackRequest::default()
10182                },
10183            )
10184            .is_err()
10185        );
10186        assert!(
10187            validate_upload_pack_request_features(
10188                &UploadPackFeatures {
10189                    side_band: true,
10190                    side_band_64k: true,
10191                    ..UploadPackFeatures::default()
10192                },
10193                &UploadPackRequest {
10194                    wants: vec![want],
10195                    capabilities: vec![
10196                        Capability {
10197                            name: "side-band".into(),
10198                            value: None,
10199                        },
10200                        Capability {
10201                            name: "side-band-64k".into(),
10202                            value: None,
10203                        },
10204                    ],
10205                    ..UploadPackRequest::default()
10206                },
10207            )
10208            .is_err()
10209        );
10210
10211        assert!(
10212            parse_upload_pack_features(&[
10213                Capability {
10214                    name: "thin-pack".into(),
10215                    value: None,
10216                },
10217                Capability {
10218                    name: "thin-pack".into(),
10219                    value: None,
10220                },
10221            ])
10222            .is_err()
10223        );
10224        assert!(
10225            encode_upload_pack_features(&UploadPackFeatures {
10226                unknown: vec![Capability {
10227                    name: "filter".into(),
10228                    value: None,
10229                }],
10230                ..UploadPackFeatures::default()
10231            })
10232            .is_err()
10233        );
10234    }
10235
10236    #[test]
10237    fn upload_pack_raw_response_builder_filters_unknown_haves_and_builds_pack() {
10238        let want = ObjectId::from_hex(
10239            ObjectFormat::Sha1,
10240            "1111111111111111111111111111111111111111",
10241        )
10242        .expect("test operation should succeed");
10243        let known_have = ObjectId::from_hex(
10244            ObjectFormat::Sha1,
10245            "2222222222222222222222222222222222222222",
10246        )
10247        .expect("test operation should succeed");
10248        let unknown_have = ObjectId::from_hex(
10249            ObjectFormat::Sha1,
10250            "3333333333333333333333333333333333333333",
10251        )
10252        .expect("test operation should succeed");
10253        let existing = std::collections::HashSet::from([want, known_have]);
10254
10255        let response = build_upload_pack_raw_packfile_response(
10256            &UploadPackFeatures::default(),
10257            UploadPackRequest {
10258                wants: vec![want],
10259                ..UploadPackRequest::default()
10260            },
10261            [known_have, unknown_have],
10262            |oid| Ok(existing.contains(oid)),
10263            |wants, haves| {
10264                assert_eq!(wants, vec![want]);
10265                assert_eq!(haves, vec![known_have]);
10266                Ok(Some(b"PACKmock".to_vec()))
10267            },
10268        )
10269        .expect("test operation should succeed");
10270
10271        assert_eq!(
10272            response.acknowledgments,
10273            vec![UploadPackAcknowledgment::Nak]
10274        );
10275        assert_eq!(response.packfile, b"PACKmock");
10276    }
10277
10278    #[test]
10279    fn upload_pack_raw_response_builder_rejects_missing_want_and_empty_pack() {
10280        let want = ObjectId::from_hex(
10281            ObjectFormat::Sha1,
10282            "1111111111111111111111111111111111111111",
10283        )
10284        .expect("test operation should succeed");
10285
10286        assert!(
10287            build_upload_pack_raw_packfile_response(
10288                &UploadPackFeatures::default(),
10289                UploadPackRequest {
10290                    wants: vec![want],
10291                    ..UploadPackRequest::default()
10292                },
10293                Vec::<ObjectId>::new(),
10294                |_| Ok(false),
10295                |_, _| Ok(Some(b"PACKmock".to_vec())),
10296            )
10297            .is_err()
10298        );
10299
10300        assert!(
10301            build_upload_pack_raw_packfile_response(
10302                &UploadPackFeatures::default(),
10303                UploadPackRequest {
10304                    wants: vec![want],
10305                    ..UploadPackRequest::default()
10306                },
10307                Vec::<ObjectId>::new(),
10308                |_| Ok(true),
10309                |_, _| Ok(None),
10310            )
10311            .is_err()
10312        );
10313    }
10314
10315    #[test]
10316    fn upload_pack_request_parses_and_encodes_initial_fetch_request() {
10317        let want = ObjectId::from_hex(
10318            ObjectFormat::Sha1,
10319            "1111111111111111111111111111111111111111",
10320        )
10321        .expect("test operation should succeed");
10322        let second_want = ObjectId::from_hex(
10323            ObjectFormat::Sha1,
10324            "2222222222222222222222222222222222222222",
10325        )
10326        .expect("test operation should succeed");
10327        let shallow = ObjectId::from_hex(
10328            ObjectFormat::Sha1,
10329            "3333333333333333333333333333333333333333",
10330        )
10331        .expect("test operation should succeed");
10332        let frames = vec![
10333            PktLineFrame::Data(
10334                b"want 1111111111111111111111111111111111111111 multi_ack thin-pack agent=git/2.54.0\n"
10335                    .to_vec(),
10336            ),
10337            PktLineFrame::Data(b"want 2222222222222222222222222222222222222222\n".to_vec()),
10338            PktLineFrame::Data(b"shallow 3333333333333333333333333333333333333333\n".to_vec()),
10339            PktLineFrame::Data(b"deepen-since 1710000000\n".to_vec()),
10340            PktLineFrame::Data(b"deepen-not refs/tags/base\n".to_vec()),
10341            PktLineFrame::Data(b"filter blob:none\n".to_vec()),
10342            PktLineFrame::Flush,
10343        ];
10344        let request = parse_upload_pack_request(ObjectFormat::Sha1, &frames)
10345            .expect("test operation should succeed")
10346            .expect("test operation should succeed");
10347        assert_eq!(
10348            request,
10349            UploadPackRequest {
10350                wants: vec![want, second_want],
10351                capabilities: vec![
10352                    Capability {
10353                        name: "multi_ack".into(),
10354                        value: None,
10355                    },
10356                    Capability {
10357                        name: "thin-pack".into(),
10358                        value: None,
10359                    },
10360                    Capability {
10361                        name: "agent".into(),
10362                        value: Some("git/2.54.0".into()),
10363                    },
10364                ],
10365                shallow: vec![shallow],
10366                deepen: None,
10367                deepen_since: Some(1_710_000_000),
10368                deepen_not: vec!["refs/tags/base".into()],
10369                filter: Some("blob:none".into()),
10370            }
10371        );
10372        assert_eq!(
10373            encode_upload_pack_request(Some(&request)).expect("test operation should succeed"),
10374            frames
10375        );
10376        assert_eq!(
10377            parse_upload_pack_request(ObjectFormat::Sha1, &[PktLineFrame::Flush])
10378                .expect("test operation should succeed"),
10379            None
10380        );
10381        assert_eq!(
10382            encode_upload_pack_request(None).expect("test operation should succeed"),
10383            vec![PktLineFrame::Flush]
10384        );
10385    }
10386
10387    #[test]
10388    fn upload_pack_request_streams_round_trip() {
10389        let request = UploadPackRequest {
10390            wants: vec![
10391                ObjectId::from_hex(
10392                    ObjectFormat::Sha1,
10393                    "1111111111111111111111111111111111111111",
10394                )
10395                .expect("test operation should succeed"),
10396            ],
10397            capabilities: vec![Capability {
10398                name: "ofs-delta".into(),
10399                value: None,
10400            }],
10401            deepen: Some(10),
10402            ..UploadPackRequest::default()
10403        };
10404        let mut encoded = Vec::new();
10405        write_upload_pack_request(&mut encoded, Some(&request))
10406            .expect("test operation should succeed");
10407        encoded.extend_from_slice(b"tail");
10408
10409        let mut input = encoded.as_slice();
10410        assert_eq!(
10411            read_upload_pack_request(ObjectFormat::Sha1, &mut input)
10412                .expect("test operation should succeed"),
10413            Some(request)
10414        );
10415        assert_eq!(input, b"tail");
10416    }
10417
10418    #[test]
10419    fn upload_pack_request_rejects_malformed_requests() {
10420        assert!(
10421            parse_upload_pack_request(
10422                ObjectFormat::Sha1,
10423                &[PktLineFrame::Data(
10424                    b"want 1111111111111111111111111111111111111111\n".to_vec(),
10425                )],
10426            )
10427            .is_err()
10428        );
10429        assert!(
10430            parse_upload_pack_request(
10431                ObjectFormat::Sha1,
10432                &[
10433                    PktLineFrame::Data(
10434                        b"shallow 1111111111111111111111111111111111111111\n".to_vec(),
10435                    ),
10436                    PktLineFrame::Flush,
10437                ],
10438            )
10439            .is_err()
10440        );
10441        assert!(
10442            parse_upload_pack_request(
10443                ObjectFormat::Sha1,
10444                &[
10445                    PktLineFrame::Data(
10446                        b"want 1111111111111111111111111111111111111111 thin-pack\n".to_vec(),
10447                    ),
10448                    PktLineFrame::Data(
10449                        b"want 2222222222222222222222222222222222222222 ofs-delta\n".to_vec(),
10450                    ),
10451                    PktLineFrame::Flush,
10452                ],
10453            )
10454            .is_err()
10455        );
10456        assert!(parse_upload_pack_request(
10457            ObjectFormat::Sha1,
10458            &[
10459                PktLineFrame::Data(b"want 1111111111111111111111111111111111111111\n".to_vec(),),
10460                PktLineFrame::Data(b"deepen 1\n".to_vec()),
10461                PktLineFrame::Data(b"want 2222222222222222222222222222222222222222\n".to_vec()),
10462                PktLineFrame::Flush,
10463            ],
10464        )
10465        .is_err());
10466        assert!(parse_upload_pack_request(
10467            ObjectFormat::Sha1,
10468            &[
10469                PktLineFrame::Data(b"want 1111111111111111111111111111111111111111\n".to_vec(),),
10470                PktLineFrame::Data(b"filter blob:none\n".to_vec()),
10471                PktLineFrame::Data(b"filter tree:0\n".to_vec()),
10472                PktLineFrame::Flush,
10473            ],
10474        )
10475        .is_err());
10476        assert!(encode_upload_pack_request(Some(&UploadPackRequest::default())).is_err());
10477        assert!(
10478            encode_upload_pack_request(Some(&UploadPackRequest {
10479                wants: vec![
10480                    ObjectId::from_hex(
10481                        ObjectFormat::Sha1,
10482                        "1111111111111111111111111111111111111111",
10483                    )
10484                    .expect("test operation should succeed")
10485                ],
10486                deepen: Some(0),
10487                ..UploadPackRequest::default()
10488            }))
10489            .is_err()
10490        );
10491    }
10492
10493    #[test]
10494    fn upload_pack_shallow_update_parses_and_encodes_records() {
10495        let shallow = ObjectId::from_hex(
10496            ObjectFormat::Sha1,
10497            "1111111111111111111111111111111111111111",
10498        )
10499        .expect("test operation should succeed");
10500        let unshallow = ObjectId::from_hex(
10501            ObjectFormat::Sha1,
10502            "2222222222222222222222222222222222222222",
10503        )
10504        .expect("test operation should succeed");
10505        let frames = vec![
10506            PktLineFrame::Data(b"shallow 1111111111111111111111111111111111111111\n".to_vec()),
10507            PktLineFrame::Data(b"unshallow 2222222222222222222222222222222222222222\n".to_vec()),
10508            PktLineFrame::Flush,
10509        ];
10510        let entries = parse_upload_pack_shallow_update(ObjectFormat::Sha1, &frames)
10511            .expect("test operation should succeed");
10512        assert_eq!(
10513            entries,
10514            vec![
10515                ProtocolV2FetchShallowInfo::Shallow(shallow),
10516                ProtocolV2FetchShallowInfo::Unshallow(unshallow),
10517            ]
10518        );
10519        assert_eq!(
10520            encode_upload_pack_shallow_update(&entries).expect("test operation should succeed"),
10521            frames
10522        );
10523        assert_eq!(
10524            parse_upload_pack_shallow_update(ObjectFormat::Sha1, &[PktLineFrame::Flush])
10525                .expect("test operation should succeed"),
10526            Vec::<ProtocolV2FetchShallowInfo>::new()
10527        );
10528    }
10529
10530    #[test]
10531    fn upload_pack_shallow_update_streams_round_trip() {
10532        let entries = vec![ProtocolV2FetchShallowInfo::Shallow(
10533            ObjectId::from_hex(
10534                ObjectFormat::Sha1,
10535                "1111111111111111111111111111111111111111",
10536            )
10537            .expect("test operation should succeed"),
10538        )];
10539        let mut encoded = Vec::new();
10540        write_upload_pack_shallow_update(&mut encoded, &entries)
10541            .expect("test operation should succeed");
10542        encoded.extend_from_slice(b"tail");
10543
10544        let mut input = encoded.as_slice();
10545        assert_eq!(
10546            read_upload_pack_shallow_update(ObjectFormat::Sha1, &mut input)
10547                .expect("test operation should succeed"),
10548            entries
10549        );
10550        assert_eq!(input, b"tail");
10551    }
10552
10553    #[test]
10554    fn upload_pack_shallow_update_rejects_malformed_records() {
10555        assert!(
10556            parse_upload_pack_shallow_update(
10557                ObjectFormat::Sha1,
10558                &[PktLineFrame::Data(
10559                    b"shallow 1111111111111111111111111111111111111111\n".to_vec(),
10560                )],
10561            )
10562            .is_err()
10563        );
10564        assert!(
10565            parse_upload_pack_shallow_update(
10566                ObjectFormat::Sha1,
10567                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
10568            )
10569            .is_err()
10570        );
10571        assert!(
10572            parse_upload_pack_shallow_update(
10573                ObjectFormat::Sha1,
10574                &[
10575                    PktLineFrame::Data(
10576                        b"shallow 1111111111111111111111111111111111111111\n".to_vec(),
10577                    ),
10578                    PktLineFrame::Flush,
10579                    PktLineFrame::Data(
10580                        b"unshallow 2222222222222222222222222222222222222222\n".to_vec(),
10581                    ),
10582                ],
10583            )
10584            .is_err()
10585        );
10586        assert!(
10587            parse_upload_pack_shallow_update(
10588                ObjectFormat::Sha1,
10589                &[
10590                    PktLineFrame::Data(
10591                        b"unsupported 1111111111111111111111111111111111111111\n".to_vec(),
10592                    ),
10593                    PktLineFrame::Flush,
10594                ],
10595            )
10596            .is_err()
10597        );
10598    }
10599
10600    #[test]
10601    fn upload_pack_negotiation_request_parses_flush_and_done_rounds() {
10602        let have = ObjectId::from_hex(
10603            ObjectFormat::Sha1,
10604            "1111111111111111111111111111111111111111",
10605        )
10606        .expect("test operation should succeed");
10607        let second_have = ObjectId::from_hex(
10608            ObjectFormat::Sha1,
10609            "2222222222222222222222222222222222222222",
10610        )
10611        .expect("test operation should succeed");
10612        let flush_round = vec![
10613            PktLineFrame::Data(b"have 1111111111111111111111111111111111111111\n".to_vec()),
10614            PktLineFrame::Data(b"have 2222222222222222222222222222222222222222\n".to_vec()),
10615            PktLineFrame::Flush,
10616        ];
10617        let request = parse_upload_pack_negotiation_request(ObjectFormat::Sha1, &flush_round)
10618            .expect("test operation should succeed");
10619        assert_eq!(
10620            request,
10621            UploadPackNegotiationRequest {
10622                haves: vec![have, second_have],
10623                done: false,
10624            }
10625        );
10626        assert_eq!(
10627            encode_upload_pack_negotiation_request(&request)
10628                .expect("test operation should succeed"),
10629            flush_round
10630        );
10631
10632        let done_round = vec![
10633            PktLineFrame::Data(b"have 1111111111111111111111111111111111111111\n".to_vec()),
10634            PktLineFrame::Data(b"done\n".to_vec()),
10635        ];
10636        let request = parse_upload_pack_negotiation_request(ObjectFormat::Sha1, &done_round)
10637            .expect("test operation should succeed");
10638        assert_eq!(
10639            request,
10640            UploadPackNegotiationRequest {
10641                haves: vec![have],
10642                done: true,
10643            }
10644        );
10645        assert_eq!(
10646            encode_upload_pack_negotiation_request(&request)
10647                .expect("test operation should succeed"),
10648            done_round
10649        );
10650    }
10651
10652    #[test]
10653    fn upload_pack_negotiation_request_streams_round_trip() {
10654        let first = UploadPackNegotiationRequest {
10655            haves: vec![
10656                ObjectId::from_hex(
10657                    ObjectFormat::Sha1,
10658                    "1111111111111111111111111111111111111111",
10659                )
10660                .expect("test operation should succeed"),
10661            ],
10662            done: false,
10663        };
10664        let second = UploadPackNegotiationRequest {
10665            haves: Vec::new(),
10666            done: true,
10667        };
10668        let mut encoded = Vec::new();
10669        write_upload_pack_negotiation_request(&mut encoded, &first)
10670            .expect("test operation should succeed");
10671        write_upload_pack_negotiation_request(&mut encoded, &second)
10672            .expect("test operation should succeed");
10673        encoded.extend_from_slice(b"tail");
10674
10675        let mut input = encoded.as_slice();
10676        assert_eq!(
10677            read_upload_pack_negotiation_request(ObjectFormat::Sha1, &mut input)
10678                .expect("test operation should succeed"),
10679            first
10680        );
10681        assert_eq!(
10682            read_upload_pack_negotiation_request(ObjectFormat::Sha1, &mut input)
10683                .expect("test operation should succeed"),
10684            second
10685        );
10686        assert_eq!(input, b"tail");
10687    }
10688
10689    #[test]
10690    fn upload_pack_negotiation_request_rejects_malformed_rounds() {
10691        assert!(
10692            parse_upload_pack_negotiation_request(
10693                ObjectFormat::Sha1,
10694                &[PktLineFrame::Data(
10695                    b"have 1111111111111111111111111111111111111111\n".to_vec(),
10696                )],
10697            )
10698            .is_err()
10699        );
10700        assert!(
10701            parse_upload_pack_negotiation_request(
10702                ObjectFormat::Sha1,
10703                &[PktLineFrame::Data(
10704                    b"want 1111111111111111111111111111111111111111\n".to_vec(),
10705                )],
10706            )
10707            .is_err()
10708        );
10709        assert!(parse_upload_pack_negotiation_request(
10710            ObjectFormat::Sha1,
10711            &[
10712                PktLineFrame::Data(b"done\n".to_vec()),
10713                PktLineFrame::Data(b"have 1111111111111111111111111111111111111111\n".to_vec(),),
10714            ],
10715        )
10716        .is_err());
10717        assert!(
10718            parse_upload_pack_negotiation_request(
10719                ObjectFormat::Sha1,
10720                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
10721            )
10722            .is_err()
10723        );
10724    }
10725
10726    #[test]
10727    fn upload_pack_acknowledgments_parse_and_encode_statuses() {
10728        let oid = ObjectId::from_hex(
10729            ObjectFormat::Sha1,
10730            "1111111111111111111111111111111111111111",
10731        )
10732        .expect("test operation should succeed");
10733        assert_eq!(
10734            parse_upload_pack_acknowledgment(ObjectFormat::Sha1, b"NAK\n")
10735                .expect("test operation should succeed"),
10736            UploadPackAcknowledgment::Nak
10737        );
10738        for (payload, status) in [
10739            (
10740                b"ACK 1111111111111111111111111111111111111111\n".as_slice(),
10741                None,
10742            ),
10743            (
10744                b"ACK 1111111111111111111111111111111111111111 continue\n".as_slice(),
10745                Some(UploadPackAckStatus::Continue),
10746            ),
10747            (
10748                b"ACK 1111111111111111111111111111111111111111 common\n".as_slice(),
10749                Some(UploadPackAckStatus::Common),
10750            ),
10751            (
10752                b"ACK 1111111111111111111111111111111111111111 ready\n".as_slice(),
10753                Some(UploadPackAckStatus::Ready),
10754            ),
10755        ] {
10756            let acknowledgment = parse_upload_pack_acknowledgment(ObjectFormat::Sha1, payload)
10757                .expect("test operation should succeed");
10758            assert_eq!(
10759                acknowledgment,
10760                UploadPackAcknowledgment::Ack { oid, status }
10761            );
10762            assert_eq!(
10763                encode_upload_pack_acknowledgment(&acknowledgment)
10764                    .expect("test operation should succeed"),
10765                payload
10766            );
10767        }
10768    }
10769
10770    #[test]
10771    fn upload_pack_acknowledgments_stream_round_trip_and_reject_bad_lines() {
10772        let acknowledgment = UploadPackAcknowledgment::Ack {
10773            oid: ObjectId::from_hex(
10774                ObjectFormat::Sha1,
10775                "1111111111111111111111111111111111111111",
10776            )
10777            .expect("test operation should succeed"),
10778            status: Some(UploadPackAckStatus::Ready),
10779        };
10780        let mut encoded = Vec::new();
10781        write_upload_pack_acknowledgment(&mut encoded, &acknowledgment)
10782            .expect("test operation should succeed");
10783        encoded.extend_from_slice(b"tail");
10784
10785        let mut input = encoded.as_slice();
10786        assert_eq!(
10787            read_upload_pack_acknowledgment(ObjectFormat::Sha1, &mut input)
10788                .expect("test operation should succeed"),
10789            acknowledgment
10790        );
10791        assert_eq!(input, b"tail");
10792        assert!(parse_upload_pack_acknowledgment(ObjectFormat::Sha1, b"ACK not-an-oid\n").is_err());
10793        assert!(
10794            parse_upload_pack_acknowledgment(
10795                ObjectFormat::Sha1,
10796                b"ACK 1111111111111111111111111111111111111111 unknown\n",
10797            )
10798            .is_err()
10799        );
10800        assert!(
10801            parse_upload_pack_acknowledgment(
10802                ObjectFormat::Sha1,
10803                b"ACK 1111111111111111111111111111111111111111 ready extra\n",
10804            )
10805            .is_err()
10806        );
10807        assert!(
10808            parse_upload_pack_acknowledgment(ObjectFormat::Sha1, b"ERR remote died\n").is_err()
10809        );
10810        assert!(read_upload_pack_acknowledgment(ObjectFormat::Sha1, &mut &b"0000"[..]).is_err());
10811    }
10812
10813    #[test]
10814    fn upload_pack_packfile_response_parses_acknowledgments_and_sideband() {
10815        let oid = ObjectId::from_hex(
10816            ObjectFormat::Sha1,
10817            "1111111111111111111111111111111111111111",
10818        )
10819        .expect("test operation should succeed");
10820        let frames = vec![
10821            PktLineFrame::Data(b"ACK 1111111111111111111111111111111111111111 common\n".to_vec()),
10822            PktLineFrame::Data(b"NAK\n".to_vec()),
10823            PktLineFrame::Data(b"\x01PACK".to_vec()),
10824            PktLineFrame::Data(b"\x02counting objects\n".to_vec()),
10825            PktLineFrame::Data(b"\x01 bytes".to_vec()),
10826            PktLineFrame::Flush,
10827        ];
10828        let response = parse_upload_pack_packfile_response(ObjectFormat::Sha1, &frames)
10829            .expect("test operation should succeed");
10830        assert_eq!(
10831            response,
10832            UploadPackPackfileResponse {
10833                acknowledgments: vec![
10834                    UploadPackAcknowledgment::Ack {
10835                        oid,
10836                        status: Some(UploadPackAckStatus::Common),
10837                    },
10838                    UploadPackAcknowledgment::Nak,
10839                ],
10840                sideband: vec![
10841                    SideBandPacket {
10842                        channel: SideBandChannel::Data,
10843                        data: b"PACK".to_vec(),
10844                    },
10845                    SideBandPacket {
10846                        channel: SideBandChannel::Progress,
10847                        data: b"counting objects\n".to_vec(),
10848                    },
10849                    SideBandPacket {
10850                        channel: SideBandChannel::Data,
10851                        data: b" bytes".to_vec(),
10852                    },
10853                ],
10854            }
10855        );
10856        assert_eq!(
10857            demux_upload_pack_packfile_response(&response).expect("test operation should succeed"),
10858            SideBandDemux {
10859                data: b"PACK bytes".to_vec(),
10860                progress: vec![b"counting objects\n".to_vec()],
10861            }
10862        );
10863        assert_eq!(
10864            encode_upload_pack_packfile_response(&response).expect("test operation should succeed"),
10865            frames
10866        );
10867    }
10868
10869    #[test]
10870    fn upload_pack_packfile_response_streams_round_trip() {
10871        let response = UploadPackPackfileResponse {
10872            acknowledgments: vec![UploadPackAcknowledgment::Nak],
10873            sideband: vec![SideBandPacket {
10874                channel: SideBandChannel::Data,
10875                data: b"PACK".to_vec(),
10876            }],
10877        };
10878        let mut encoded = Vec::new();
10879        write_upload_pack_packfile_response(&mut encoded, &response)
10880            .expect("test operation should succeed");
10881        encoded.extend_from_slice(b"tail");
10882
10883        let mut input = encoded.as_slice();
10884        assert_eq!(
10885            read_upload_pack_packfile_response(ObjectFormat::Sha1, &mut input)
10886                .expect("test operation should succeed"),
10887            response
10888        );
10889        assert_eq!(input, b"tail");
10890    }
10891
10892    #[test]
10893    fn upload_pack_packfile_response_rejects_malformed_streams() {
10894        assert!(
10895            parse_upload_pack_packfile_response(
10896                ObjectFormat::Sha1,
10897                &[PktLineFrame::Data(b"NAK\n".to_vec())],
10898            )
10899            .is_err()
10900        );
10901        assert!(
10902            parse_upload_pack_packfile_response(
10903                ObjectFormat::Sha1,
10904                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
10905            )
10906            .is_err()
10907        );
10908        assert!(
10909            parse_upload_pack_packfile_response(
10910                ObjectFormat::Sha1,
10911                &[
10912                    PktLineFrame::Data(b"\x01PACK".to_vec()),
10913                    PktLineFrame::Data(
10914                        b"ACK 1111111111111111111111111111111111111111 common\n".to_vec()
10915                    ),
10916                    PktLineFrame::Flush,
10917                ],
10918            )
10919            .is_err()
10920        );
10921        assert!(
10922            parse_upload_pack_packfile_response(
10923                ObjectFormat::Sha1,
10924                &[
10925                    PktLineFrame::Data(b"NAK\n".to_vec()),
10926                    PktLineFrame::Flush,
10927                    PktLineFrame::Data(b"\x01PACK".to_vec()),
10928                ],
10929            )
10930            .is_err()
10931        );
10932        assert!(
10933            parse_upload_pack_packfile_response(
10934                ObjectFormat::Sha1,
10935                &[
10936                    PktLineFrame::Data(b"NAK\n".to_vec()),
10937                    PktLineFrame::Data(b"\x04bad".to_vec()),
10938                    PktLineFrame::Flush,
10939                ],
10940            )
10941            .is_err()
10942        );
10943    }
10944
10945    #[test]
10946    fn upload_pack_raw_packfile_response_parses_acknowledgments_and_raw_pack() {
10947        let oid = ObjectId::from_hex(
10948            ObjectFormat::Sha1,
10949            "1111111111111111111111111111111111111111",
10950        )
10951        .expect("test operation should succeed");
10952        let response = UploadPackRawPackfileResponse {
10953            acknowledgments: vec![
10954                UploadPackAcknowledgment::Ack {
10955                    oid,
10956                    status: Some(UploadPackAckStatus::Common),
10957                },
10958                UploadPackAcknowledgment::Nak,
10959            ],
10960            packfile: b"PACK\x00\x00\x00\x02raw-bytes".to_vec(),
10961        };
10962        let encoded = encode_upload_pack_raw_packfile_response(&response)
10963            .expect("test operation should succeed");
10964        assert_eq!(
10965            parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &encoded)
10966                .expect("test operation should succeed"),
10967            response
10968        );
10969    }
10970
10971    #[test]
10972    fn upload_pack_raw_packfile_response_streams_round_trip() {
10973        let response = UploadPackRawPackfileResponse {
10974            acknowledgments: vec![UploadPackAcknowledgment::Nak],
10975            packfile: b"PACK\x00\x00\x00\x02raw-bytes".to_vec(),
10976        };
10977        let mut encoded = Vec::new();
10978        write_upload_pack_raw_packfile_response(&mut encoded, &response)
10979            .expect("test operation should succeed");
10980        assert_eq!(
10981            encoded,
10982            encode_upload_pack_raw_packfile_response(&response)
10983                .expect("test operation should succeed")
10984        );
10985
10986        let mut input = encoded.as_slice();
10987        assert_eq!(
10988            read_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &mut input)
10989                .expect("test operation should succeed"),
10990            response
10991        );
10992        assert!(input.is_empty());
10993    }
10994
10995    #[test]
10996    fn upload_pack_raw_packfile_response_rejects_malformed_streams() {
10997        let ack = PktLineFrame::data(b"NAK\n".to_vec())
10998            .expect("test operation should succeed")
10999            .try_encode()
11000            .expect("test operation should succeed");
11001        let bad_ack = PktLineFrame::data(b"ACK not-an-oid\n".to_vec())
11002            .expect("test operation should succeed")
11003            .try_encode()
11004            .expect("test operation should succeed");
11005        let non_ack =
11006            PktLineFrame::data(b"have 1111111111111111111111111111111111111111\n".to_vec())
11007                .expect("test operation should succeed")
11008                .try_encode()
11009                .expect("test operation should succeed");
11010        let mut garbage_after_ack = ack.clone();
11011        garbage_after_ack.extend_from_slice(b"garbage");
11012
11013        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, b"").is_err());
11014        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &ack).is_err());
11015        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &bad_ack).is_err());
11016        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, b"0000PACK").is_err());
11017        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &non_ack).is_err());
11018        assert!(
11019            parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &garbage_after_ack)
11020                .is_err()
11021        );
11022        assert!(
11023            encode_upload_pack_raw_packfile_response(&UploadPackRawPackfileResponse {
11024                acknowledgments: vec![UploadPackAcknowledgment::Nak],
11025                packfile: Vec::new(),
11026            })
11027            .is_err()
11028        );
11029        assert!(
11030            encode_upload_pack_raw_packfile_response(&UploadPackRawPackfileResponse {
11031                acknowledgments: Vec::new(),
11032                packfile: b"not-a-pack".to_vec(),
11033            })
11034            .is_err()
11035        );
11036    }
11037
11038    #[test]
11039    fn upload_pack_request_encodes_deepen_request() {
11040        // A `--depth 1` clone over smart-HTTP v1: the `want` line carries the
11041        // capabilities, the client's existing shallow boundary is replayed as a
11042        // `shallow` line, and `deepen 1` requests the truncation. Built as raw
11043        // pkt-line bytes so the 4-hex length prefixes are exercised.
11044        let want = ObjectId::from_hex(
11045            ObjectFormat::Sha1,
11046            "1111111111111111111111111111111111111111",
11047        )
11048        .expect("test operation should succeed");
11049        let boundary = ObjectId::from_hex(
11050            ObjectFormat::Sha1,
11051            "2222222222222222222222222222222222222222",
11052        )
11053        .expect("test operation should succeed");
11054        let request = UploadPackRequest {
11055            wants: vec![want],
11056            capabilities: vec![Capability {
11057                name: "shallow".into(),
11058                value: None,
11059            }],
11060            shallow: vec![boundary],
11061            deepen: Some(1),
11062            ..UploadPackRequest::default()
11063        };
11064        let mut encoded = Vec::new();
11065        write_upload_pack_request(&mut encoded, Some(&request))
11066            .expect("test operation should succeed");
11067        let mut expected = Vec::new();
11068        expected.extend_from_slice(b"003awant 1111111111111111111111111111111111111111 shallow\n");
11069        expected.extend_from_slice(b"0035shallow 2222222222222222222222222222222222222222\n");
11070        expected.extend_from_slice(b"000ddeepen 1\n");
11071        expected.extend_from_slice(b"0000");
11072        assert_eq!(encoded, expected);
11073    }
11074
11075    #[test]
11076    fn upload_pack_shallow_info_response_parses_shallow_unshallow_and_pack() {
11077        // The smart-HTTP v1 deepen response: a shallow-info section (one
11078        // `shallow` and one `unshallow` line) terminated by a flush, then the
11079        // NAK and the raw packfile. Hand-built pkt-lines (mind the lengths).
11080        let shallow = ObjectId::from_hex(
11081            ObjectFormat::Sha1,
11082            "1111111111111111111111111111111111111111",
11083        )
11084        .expect("test operation should succeed");
11085        let unshallow = ObjectId::from_hex(
11086            ObjectFormat::Sha1,
11087            "2222222222222222222222222222222222222222",
11088        )
11089        .expect("test operation should succeed");
11090        let mut input = Vec::new();
11091        input.extend_from_slice(b"0035shallow 1111111111111111111111111111111111111111\n");
11092        input.extend_from_slice(b"0037unshallow 2222222222222222222222222222222222222222\n");
11093        input.extend_from_slice(b"0000"); // shallow-info terminator
11094        input.extend_from_slice(b"0008NAK\n");
11095        input.extend_from_slice(b"PACK\x00\x00\x00\x02raw-bytes");
11096
11097        let (entries, response) =
11098            parse_upload_pack_shallow_info_and_raw_packfile_response(ObjectFormat::Sha1, &input)
11099                .expect("test operation should succeed");
11100        assert_eq!(
11101            entries,
11102            vec![
11103                ProtocolV2FetchShallowInfo::Shallow(shallow),
11104                ProtocolV2FetchShallowInfo::Unshallow(unshallow),
11105            ]
11106        );
11107        assert_eq!(
11108            response,
11109            UploadPackRawPackfileResponse {
11110                acknowledgments: vec![UploadPackAcknowledgment::Nak],
11111                packfile: b"PACK\x00\x00\x00\x02raw-bytes".to_vec(),
11112            }
11113        );
11114
11115        // The reader entry point yields the same result over a stream.
11116        let mut stream = input.as_slice();
11117        let (read_entries, read_response) =
11118            read_upload_pack_shallow_info_and_raw_packfile_response(
11119                ObjectFormat::Sha1,
11120                &mut stream,
11121            )
11122            .expect("test operation should succeed");
11123        assert_eq!(read_entries, entries);
11124        assert_eq!(read_response, response);
11125    }
11126
11127    #[test]
11128    fn upload_pack_shallow_info_response_handles_empty_shallow_section() {
11129        // A deepen request that creates no boundary change still gets an empty
11130        // shallow-info section (a bare flush) before the NAK + pack.
11131        let mut input = Vec::new();
11132        input.extend_from_slice(b"0000"); // empty shallow-info
11133        input.extend_from_slice(b"0008NAK\n");
11134        input.extend_from_slice(b"PACK\x00\x00\x00\x02raw-bytes");
11135
11136        let (entries, response) =
11137            parse_upload_pack_shallow_info_and_raw_packfile_response(ObjectFormat::Sha1, &input)
11138                .expect("test operation should succeed");
11139        assert!(entries.is_empty());
11140        assert_eq!(
11141            response.acknowledgments,
11142            vec![UploadPackAcknowledgment::Nak]
11143        );
11144        assert!(response.packfile.starts_with(b"PACK"));
11145    }
11146
11147    #[test]
11148    fn upload_pack_shallow_info_response_rejects_malformed_sections() {
11149        // Truncated section (no terminating flush before EOF).
11150        let truncated = b"0035shallow 1111111111111111111111111111111111111111\n".to_vec();
11151        assert!(
11152            parse_upload_pack_shallow_info_and_raw_packfile_response(
11153                ObjectFormat::Sha1,
11154                &truncated
11155            )
11156            .is_err()
11157        );
11158        // A non-flush control packet inside the shallow-info section.
11159        let mut delimiter_section = Vec::new();
11160        delimiter_section.extend_from_slice(b"0001"); // delimiter, not a flush
11161        assert!(
11162            parse_upload_pack_shallow_info_section(ObjectFormat::Sha1, &delimiter_section).is_err()
11163        );
11164        // A non-shallow data line inside the section.
11165        let mut bad_line = Vec::new();
11166        bad_line.extend_from_slice(b"0008NAK\n");
11167        assert!(parse_upload_pack_shallow_info_section(ObjectFormat::Sha1, &bad_line).is_err());
11168        // Valid shallow-info but a missing packfile afterwards.
11169        let mut no_pack = Vec::new();
11170        no_pack.extend_from_slice(b"0000"); // empty shallow-info
11171        no_pack.extend_from_slice(b"0008NAK\n");
11172        assert!(
11173            parse_upload_pack_shallow_info_and_raw_packfile_response(ObjectFormat::Sha1, &no_pack)
11174                .is_err()
11175        );
11176    }
11177
11178    #[test]
11179    fn receive_pack_request_parses_and_encodes_commands() {
11180        let old_id = ObjectId::from_hex(
11181            ObjectFormat::Sha1,
11182            "1111111111111111111111111111111111111111",
11183        )
11184        .expect("test operation should succeed");
11185        let new_id = ObjectId::from_hex(
11186            ObjectFormat::Sha1,
11187            "2222222222222222222222222222222222222222",
11188        )
11189        .expect("test operation should succeed");
11190        let delete_old_id = ObjectId::from_hex(
11191            ObjectFormat::Sha1,
11192            "3333333333333333333333333333333333333333",
11193        )
11194        .expect("test operation should succeed");
11195        let zero = ObjectId::from_hex(
11196            ObjectFormat::Sha1,
11197            "0000000000000000000000000000000000000000",
11198        )
11199        .expect("test operation should succeed");
11200        let shallow = ObjectId::from_hex(
11201            ObjectFormat::Sha1,
11202            "4444444444444444444444444444444444444444",
11203        )
11204        .expect("test operation should succeed");
11205        let frames = vec![
11206            PktLineFrame::Data(b"shallow 4444444444444444444444444444444444444444\n".to_vec()),
11207            PktLineFrame::Data(
11208                b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\0report-status side-band-64k agent=git/2.54.0\n"
11209                    .to_vec(),
11210            ),
11211            PktLineFrame::Data(
11212                b"3333333333333333333333333333333333333333 0000000000000000000000000000000000000000 refs/heads/old\n"
11213                    .to_vec(),
11214            ),
11215            PktLineFrame::Flush,
11216        ];
11217        let request = parse_receive_pack_request(ObjectFormat::Sha1, &frames)
11218            .expect("test operation should succeed");
11219        assert_eq!(
11220            request,
11221            ReceivePackRequest {
11222                shallow: vec![shallow],
11223                commands: vec![
11224                    ReceivePackCommand {
11225                        old_id,
11226                        new_id,
11227                        name: "refs/heads/main".into(),
11228                    },
11229                    ReceivePackCommand {
11230                        old_id: delete_old_id,
11231                        new_id: zero,
11232                        name: "refs/heads/old".into(),
11233                    },
11234                ],
11235                capabilities: vec![
11236                    Capability {
11237                        name: "report-status".into(),
11238                        value: None,
11239                    },
11240                    Capability {
11241                        name: "side-band-64k".into(),
11242                        value: None,
11243                    },
11244                    Capability {
11245                        name: "agent".into(),
11246                        value: Some("git/2.54.0".into()),
11247                    },
11248                ],
11249            }
11250        );
11251        assert_eq!(
11252            encode_receive_pack_request(&request).expect("test operation should succeed"),
11253            frames
11254        );
11255        assert_eq!(
11256            parse_receive_pack_request(ObjectFormat::Sha1, &[PktLineFrame::Flush])
11257                .expect("test operation should succeed"),
11258            ReceivePackRequest::default()
11259        );
11260    }
11261
11262    #[test]
11263    fn receive_pack_request_streams_round_trip() {
11264        let request = ReceivePackRequest {
11265            commands: vec![ReceivePackCommand {
11266                old_id: ObjectId::from_hex(
11267                    ObjectFormat::Sha1,
11268                    "0000000000000000000000000000000000000000",
11269                )
11270                .expect("test operation should succeed"),
11271                new_id: ObjectId::from_hex(
11272                    ObjectFormat::Sha1,
11273                    "1111111111111111111111111111111111111111",
11274                )
11275                .expect("test operation should succeed"),
11276                name: "refs/heads/main".into(),
11277            }],
11278            capabilities: vec![Capability {
11279                name: "report-status".into(),
11280                value: None,
11281            }],
11282            ..ReceivePackRequest::default()
11283        };
11284        let mut encoded = Vec::new();
11285        write_receive_pack_request(&mut encoded, &request).expect("test operation should succeed");
11286        encoded.extend_from_slice(b"PACK");
11287
11288        let mut input = encoded.as_slice();
11289        assert_eq!(
11290            read_receive_pack_request(ObjectFormat::Sha1, &mut input)
11291                .expect("test operation should succeed"),
11292            request
11293        );
11294        assert_eq!(input, b"PACK");
11295    }
11296
11297    #[test]
11298    fn receive_pack_request_rejects_malformed_commands() {
11299        assert!(
11300            parse_receive_pack_request(
11301                ObjectFormat::Sha1,
11302                &[PktLineFrame::Data(
11303                    b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\n"
11304                        .to_vec(),
11305                )],
11306            )
11307            .is_err()
11308        );
11309        assert!(
11310            parse_receive_pack_request(
11311                ObjectFormat::Sha1,
11312                &[
11313                    PktLineFrame::Data(
11314                        b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\n"
11315                            .to_vec(),
11316                    ),
11317                    PktLineFrame::Data(
11318                        b"shallow 3333333333333333333333333333333333333333\n".to_vec(),
11319                    ),
11320                    PktLineFrame::Flush,
11321                ],
11322            )
11323            .is_err()
11324        );
11325        assert!(
11326            parse_receive_pack_request(
11327                ObjectFormat::Sha1,
11328                &[
11329                    PktLineFrame::Data(
11330                        b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\0report-status\n"
11331                            .to_vec(),
11332                    ),
11333                    PktLineFrame::Data(
11334                        b"3333333333333333333333333333333333333333 4444444444444444444444444444444444444444 refs/heads/next\0side-band-64k\n"
11335                            .to_vec(),
11336                    ),
11337                    PktLineFrame::Flush,
11338                ],
11339            )
11340            .is_err()
11341        );
11342        assert!(
11343            parse_receive_pack_request(
11344                ObjectFormat::Sha1,
11345                &[
11346                    PktLineFrame::Data(
11347                        b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec(),
11348                    ),
11349                    PktLineFrame::Flush,
11350                ],
11351            )
11352            .is_err()
11353        );
11354        assert!(
11355            encode_receive_pack_request(&ReceivePackRequest {
11356                shallow: vec![
11357                    ObjectId::from_hex(
11358                        ObjectFormat::Sha1,
11359                        "1111111111111111111111111111111111111111",
11360                    )
11361                    .expect("test operation should succeed")
11362                ],
11363                ..ReceivePackRequest::default()
11364            })
11365            .is_err()
11366        );
11367        assert!(
11368            encode_receive_pack_request(&ReceivePackRequest {
11369                commands: vec![ReceivePackCommand {
11370                    old_id: ObjectId::from_hex(
11371                        ObjectFormat::Sha1,
11372                        "1111111111111111111111111111111111111111",
11373                    )
11374                    .expect("test operation should succeed"),
11375                    new_id: ObjectId::from_hex(
11376                        ObjectFormat::Sha1,
11377                        "2222222222222222222222222222222222222222",
11378                    )
11379                    .expect("test operation should succeed"),
11380                    name: "bad ref".into(),
11381                }],
11382                ..ReceivePackRequest::default()
11383            })
11384            .is_err()
11385        );
11386    }
11387
11388    #[test]
11389    fn receive_pack_features_parse_encode_and_validate_push_request() {
11390        let capabilities = vec![
11391            Capability {
11392                name: "report-status".into(),
11393                value: None,
11394            },
11395            Capability {
11396                name: "report-status-v2".into(),
11397                value: None,
11398            },
11399            Capability {
11400                name: "delete-refs".into(),
11401                value: None,
11402            },
11403            Capability {
11404                name: "ofs-delta".into(),
11405                value: None,
11406            },
11407            Capability {
11408                name: "atomic".into(),
11409                value: None,
11410            },
11411            Capability {
11412                name: "push-options".into(),
11413                value: None,
11414            },
11415            Capability {
11416                name: "side-band-64k".into(),
11417                value: None,
11418            },
11419            Capability {
11420                name: "quiet".into(),
11421                value: None,
11422            },
11423            Capability {
11424                name: "no-thin".into(),
11425                value: None,
11426            },
11427            Capability {
11428                name: "agent".into(),
11429                value: Some("git/2.54.0".into()),
11430            },
11431            Capability {
11432                name: "object-format".into(),
11433                value: Some("sha256".into()),
11434            },
11435            Capability {
11436                name: "custom".into(),
11437                value: Some("value".into()),
11438            },
11439        ];
11440        let features =
11441            parse_receive_pack_features(&capabilities).expect("test operation should succeed");
11442        assert_eq!(
11443            features,
11444            ReceivePackFeatures {
11445                report_status: true,
11446                report_status_v2: true,
11447                delete_refs: true,
11448                ofs_delta: true,
11449                atomic: true,
11450                push_options: true,
11451                side_band_64k: true,
11452                quiet: true,
11453                no_thin: true,
11454                agent: Some("git/2.54.0".into()),
11455                object_format: Some(ObjectFormat::Sha256),
11456                unknown: vec![Capability {
11457                    name: "custom".into(),
11458                    value: Some("value".into()),
11459                }],
11460            }
11461        );
11462        assert_eq!(
11463            encode_receive_pack_features(&features).expect("test operation should succeed"),
11464            capabilities
11465        );
11466
11467        let request = ReceivePackPushRequest {
11468            commands: ReceivePackRequest {
11469                commands: vec![ReceivePackCommand {
11470                    old_id: ObjectId::from_hex(
11471                        ObjectFormat::Sha1,
11472                        "1111111111111111111111111111111111111111",
11473                    )
11474                    .expect("test operation should succeed"),
11475                    new_id: ObjectId::from_hex(
11476                        ObjectFormat::Sha1,
11477                        "2222222222222222222222222222222222222222",
11478                    )
11479                    .expect("test operation should succeed"),
11480                    name: "refs/heads/main".into(),
11481                }],
11482                capabilities: vec![
11483                    Capability {
11484                        name: "report-status".into(),
11485                        value: None,
11486                    },
11487                    Capability {
11488                        name: "ofs-delta".into(),
11489                        value: None,
11490                    },
11491                    Capability {
11492                        name: "push-options".into(),
11493                        value: None,
11494                    },
11495                    Capability {
11496                        name: "side-band-64k".into(),
11497                        value: None,
11498                    },
11499                    Capability {
11500                        name: "agent".into(),
11501                        value: Some("sley".into()),
11502                    },
11503                ],
11504                ..ReceivePackRequest::default()
11505            },
11506            push_options: Some(vec!["ci.skip".into()]),
11507            packfile: b"PACKpayload".to_vec(),
11508        };
11509        validate_receive_pack_push_request_features(&features, &request)
11510            .expect("test operation should succeed");
11511    }
11512
11513    #[test]
11514    fn receive_pack_features_reject_invalid_push_requests() {
11515        let old_id = ObjectId::from_hex(
11516            ObjectFormat::Sha1,
11517            "1111111111111111111111111111111111111111",
11518        )
11519        .expect("test operation should succeed");
11520        let new_id = ObjectId::from_hex(
11521            ObjectFormat::Sha1,
11522            "2222222222222222222222222222222222222222",
11523        )
11524        .expect("test operation should succeed");
11525        let zero = ObjectId::from_hex(
11526            ObjectFormat::Sha1,
11527            "0000000000000000000000000000000000000000",
11528        )
11529        .expect("test operation should succeed");
11530        let features = ReceivePackFeatures {
11531            report_status: true,
11532            push_options: true,
11533            ..ReceivePackFeatures::default()
11534        };
11535        let update = ReceivePackCommand {
11536            old_id: old_id.clone(),
11537            new_id: new_id.clone(),
11538            name: "refs/heads/main".into(),
11539        };
11540
11541        assert!(
11542            validate_receive_pack_push_request_features(
11543                &features,
11544                &ReceivePackPushRequest {
11545                    commands: ReceivePackRequest {
11546                        commands: vec![update.clone()],
11547                        capabilities: vec![Capability {
11548                            name: "push-options".into(),
11549                            value: None,
11550                        }],
11551                        ..ReceivePackRequest::default()
11552                    },
11553                    push_options: None,
11554                    packfile: b"PACKpayload".to_vec(),
11555                },
11556            )
11557            .is_err()
11558        );
11559        assert!(
11560            validate_receive_pack_push_request_features(
11561                &features,
11562                &ReceivePackPushRequest {
11563                    commands: ReceivePackRequest {
11564                        commands: vec![update.clone()],
11565                        ..ReceivePackRequest::default()
11566                    },
11567                    push_options: Some(Vec::new()),
11568                    packfile: b"PACKpayload".to_vec(),
11569                },
11570            )
11571            .is_err()
11572        );
11573        assert!(
11574            validate_receive_pack_push_request_features(
11575                &features,
11576                &ReceivePackPushRequest {
11577                    commands: ReceivePackRequest {
11578                        commands: vec![ReceivePackCommand {
11579                            old_id: old_id.clone(),
11580                            new_id: zero.clone(),
11581                            name: "refs/heads/main".into(),
11582                        }],
11583                        ..ReceivePackRequest::default()
11584                    },
11585                    push_options: None,
11586                    packfile: Vec::new(),
11587                },
11588            )
11589            .is_err()
11590        );
11591        assert!(
11592            validate_receive_pack_push_request_features(
11593                &features,
11594                &ReceivePackPushRequest {
11595                    commands: ReceivePackRequest {
11596                        commands: vec![update.clone()],
11597                        ..ReceivePackRequest::default()
11598                    },
11599                    push_options: None,
11600                    packfile: Vec::new(),
11601                },
11602            )
11603            .is_err()
11604        );
11605        assert!(
11606            validate_receive_pack_push_request_features(
11607                &ReceivePackFeatures {
11608                    delete_refs: true,
11609                    ..ReceivePackFeatures::default()
11610                },
11611                &ReceivePackPushRequest {
11612                    commands: ReceivePackRequest {
11613                        commands: vec![ReceivePackCommand {
11614                            old_id,
11615                            new_id: zero,
11616                            name: "refs/heads/main".into(),
11617                        }],
11618                        ..ReceivePackRequest::default()
11619                    },
11620                    push_options: None,
11621                    packfile: b"PACKpayload".to_vec(),
11622                },
11623            )
11624            .is_err()
11625        );
11626        assert!(
11627            validate_receive_pack_push_request_features(
11628                &features,
11629                &ReceivePackPushRequest {
11630                    commands: ReceivePackRequest {
11631                        commands: vec![update],
11632                        capabilities: vec![Capability {
11633                            name: "atomic".into(),
11634                            value: None,
11635                        }],
11636                        ..ReceivePackRequest::default()
11637                    },
11638                    push_options: None,
11639                    packfile: b"PACKpayload".to_vec(),
11640                },
11641            )
11642            .is_err()
11643        );
11644
11645        assert!(
11646            parse_receive_pack_features(&[
11647                Capability {
11648                    name: "push-options".into(),
11649                    value: None,
11650                },
11651                Capability {
11652                    name: "push-options".into(),
11653                    value: None,
11654                },
11655            ])
11656            .is_err()
11657        );
11658        assert!(
11659            encode_receive_pack_features(&ReceivePackFeatures {
11660                unknown: vec![Capability {
11661                    name: "atomic".into(),
11662                    value: None,
11663                }],
11664                ..ReceivePackFeatures::default()
11665            })
11666            .is_err()
11667        );
11668    }
11669
11670    #[test]
11671    fn receive_pack_apply_helper_installs_pack_verifies_objects_and_reports_ok() {
11672        let old_id = ObjectId::from_hex(
11673            ObjectFormat::Sha1,
11674            "1111111111111111111111111111111111111111",
11675        )
11676        .expect("test operation should succeed");
11677        let new_id = ObjectId::from_hex(
11678            ObjectFormat::Sha1,
11679            "2222222222222222222222222222222222222222",
11680        )
11681        .expect("test operation should succeed");
11682        let request = ReceivePackPushRequest {
11683            commands: ReceivePackRequest {
11684                commands: vec![ReceivePackCommand {
11685                    old_id: old_id.clone(),
11686                    new_id: new_id.clone(),
11687                    name: "refs/heads/main".into(),
11688                }],
11689                ..ReceivePackRequest::default()
11690            },
11691            packfile: b"PACKpayload".to_vec(),
11692            ..ReceivePackPushRequest::default()
11693        };
11694        let installed = std::cell::Cell::new(false);
11695        let applied = std::cell::RefCell::new(Vec::new());
11696
11697        let report = apply_receive_pack_push_request(
11698            &ReceivePackFeatures::default(),
11699            &request,
11700            |_| unreachable!("update stale-old checks belong to the ref transaction callback"),
11701            |packfile| {
11702                assert_eq!(packfile, b"PACKpayload");
11703                installed.set(true);
11704                Ok(())
11705            },
11706            |oid| Ok(oid == &new_id),
11707            |commands| {
11708                applied.borrow_mut().extend_from_slice(commands);
11709                Ok(())
11710            },
11711            |_| unreachable!("no delete command should be applied"),
11712        )
11713        .expect("test operation should succeed");
11714
11715        assert!(installed.get());
11716        assert_eq!(applied.into_inner(), request.commands.commands);
11717        assert_eq!(report.unpack, ReceivePackUnpackStatus::Ok);
11718        assert_eq!(
11719            report.commands,
11720            vec![ReceivePackCommandStatus::Ok {
11721                name: "refs/heads/main".into(),
11722            }]
11723        );
11724    }
11725
11726    #[test]
11727    fn receive_pack_apply_helper_preserves_delete_only_and_stale_delete_rules() {
11728        let old_id = ObjectId::from_hex(
11729            ObjectFormat::Sha1,
11730            "1111111111111111111111111111111111111111",
11731        )
11732        .expect("test operation should succeed");
11733        let other_id = ObjectId::from_hex(
11734            ObjectFormat::Sha1,
11735            "2222222222222222222222222222222222222222",
11736        )
11737        .expect("test operation should succeed");
11738        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
11739        let request = ReceivePackPushRequest {
11740            commands: ReceivePackRequest {
11741                commands: vec![ReceivePackCommand {
11742                    old_id: old_id.clone(),
11743                    new_id: zero,
11744                    name: "refs/heads/main".into(),
11745                }],
11746                ..ReceivePackRequest::default()
11747            },
11748            ..ReceivePackPushRequest::default()
11749        };
11750        let features = ReceivePackFeatures {
11751            delete_refs: true,
11752            ..ReceivePackFeatures::default()
11753        };
11754        let installed = std::cell::Cell::new(false);
11755        let deleted = std::cell::RefCell::new(Vec::new());
11756
11757        let report = apply_receive_pack_push_request(
11758            &features,
11759            &request,
11760            |_| Ok(Some(old_id.clone())),
11761            |_| {
11762                installed.set(true);
11763                Ok(())
11764            },
11765            |_| Ok(false),
11766            |_| unreachable!("delete-only request should not apply updates"),
11767            |command| {
11768                deleted.borrow_mut().push(command.name.clone());
11769                Ok(())
11770            },
11771        )
11772        .expect("test operation should succeed");
11773
11774        assert!(!installed.get());
11775        assert_eq!(deleted.into_inner(), vec!["refs/heads/main"]);
11776        assert_eq!(report.unpack, ReceivePackUnpackStatus::Ok);
11777        assert!(
11778            apply_receive_pack_push_request(
11779                &features,
11780                &request,
11781                |_| Ok(Some(other_id.clone())),
11782                |_| Ok(()),
11783                |_| Ok(false),
11784                |_| Ok(()),
11785                |_| Ok(()),
11786            )
11787            .is_err()
11788        );
11789    }
11790
11791    #[test]
11792    fn receive_pack_push_request_parses_commands_options_and_packfile() {
11793        let command = ReceivePackCommand {
11794            old_id: ObjectId::from_hex(
11795                ObjectFormat::Sha1,
11796                "1111111111111111111111111111111111111111",
11797            )
11798            .expect("test operation should succeed"),
11799            new_id: ObjectId::from_hex(
11800                ObjectFormat::Sha1,
11801                "2222222222222222222222222222222222222222",
11802            )
11803            .expect("test operation should succeed"),
11804            name: "refs/heads/main".into(),
11805        };
11806        let expected = ReceivePackPushRequest {
11807            commands: ReceivePackRequest {
11808                commands: vec![command],
11809                capabilities: vec![
11810                    Capability {
11811                        name: "report-status".into(),
11812                        value: None,
11813                    },
11814                    Capability {
11815                        name: "push-options".into(),
11816                        value: None,
11817                    },
11818                ],
11819                ..ReceivePackRequest::default()
11820            },
11821            push_options: Some(vec!["ci.skip".into(), "deploy=staging".into()]),
11822            packfile: b"PACK\x00\x00\x00\x02payload".to_vec(),
11823        };
11824        let encoded =
11825            encode_receive_pack_push_request(&expected).expect("test operation should succeed");
11826
11827        assert_eq!(
11828            parse_receive_pack_push_request(ObjectFormat::Sha1, &encoded, true)
11829                .expect("test operation should succeed"),
11830            expected
11831        );
11832    }
11833
11834    #[test]
11835    fn receive_pack_push_request_preserves_packfile_without_push_options() {
11836        let request = ReceivePackPushRequest {
11837            commands: ReceivePackRequest {
11838                commands: vec![ReceivePackCommand {
11839                    old_id: ObjectId::from_hex(
11840                        ObjectFormat::Sha1,
11841                        "1111111111111111111111111111111111111111",
11842                    )
11843                    .expect("test operation should succeed"),
11844                    new_id: ObjectId::from_hex(
11845                        ObjectFormat::Sha1,
11846                        "2222222222222222222222222222222222222222",
11847                    )
11848                    .expect("test operation should succeed"),
11849                    name: "refs/heads/main".into(),
11850                }],
11851                ..ReceivePackRequest::default()
11852            },
11853            push_options: None,
11854            packfile: b"0000PACK-like bytes stay raw".to_vec(),
11855        };
11856        let encoded =
11857            encode_receive_pack_push_request(&request).expect("test operation should succeed");
11858
11859        assert_eq!(
11860            parse_receive_pack_push_request(ObjectFormat::Sha1, &encoded, false)
11861                .expect("test operation should succeed"),
11862            request
11863        );
11864    }
11865
11866    #[test]
11867    fn receive_pack_push_request_streams_round_trip() {
11868        let request = ReceivePackPushRequest {
11869            commands: ReceivePackRequest {
11870                commands: vec![ReceivePackCommand {
11871                    old_id: ObjectId::from_hex(
11872                        ObjectFormat::Sha1,
11873                        "1111111111111111111111111111111111111111",
11874                    )
11875                    .expect("test operation should succeed"),
11876                    new_id: ObjectId::from_hex(
11877                        ObjectFormat::Sha1,
11878                        "2222222222222222222222222222222222222222",
11879                    )
11880                    .expect("test operation should succeed"),
11881                    name: "refs/heads/main".into(),
11882                }],
11883                capabilities: vec![Capability {
11884                    name: "push-options".into(),
11885                    value: None,
11886                }],
11887                ..ReceivePackRequest::default()
11888            },
11889            push_options: Some(Vec::new()),
11890            packfile: b"PACKpayload".to_vec(),
11891        };
11892        let mut encoded = Vec::new();
11893        write_receive_pack_push_request(&mut encoded, &request)
11894            .expect("test operation should succeed");
11895
11896        assert_eq!(
11897            read_receive_pack_push_request(ObjectFormat::Sha1, &mut encoded.as_slice(), true)
11898                .expect("test operation should succeed"),
11899            request
11900        );
11901    }
11902
11903    #[test]
11904    fn receive_pack_push_request_rejects_malformed_sections() {
11905        assert!(
11906            parse_receive_pack_push_request(
11907                ObjectFormat::Sha1,
11908                b"0014not-a-command\n0000PACK",
11909                false,
11910            )
11911            .is_err()
11912        );
11913
11914        let request = ReceivePackPushRequest {
11915            commands: ReceivePackRequest {
11916                commands: vec![ReceivePackCommand {
11917                    old_id: ObjectId::from_hex(
11918                        ObjectFormat::Sha1,
11919                        "1111111111111111111111111111111111111111",
11920                    )
11921                    .expect("test operation should succeed"),
11922                    new_id: ObjectId::from_hex(
11923                        ObjectFormat::Sha1,
11924                        "2222222222222222222222222222222222222222",
11925                    )
11926                    .expect("test operation should succeed"),
11927                    name: "refs/heads/main".into(),
11928                }],
11929                ..ReceivePackRequest::default()
11930            },
11931            push_options: None,
11932            packfile: b"PACKpayload".to_vec(),
11933        };
11934        let encoded =
11935            encode_receive_pack_push_request(&request).expect("test operation should succeed");
11936        assert!(parse_receive_pack_push_request(ObjectFormat::Sha1, &encoded, true).is_err());
11937
11938        assert!(
11939            encode_receive_pack_push_request(&ReceivePackPushRequest {
11940                commands: ReceivePackRequest {
11941                    shallow: vec![
11942                        ObjectId::from_hex(
11943                            ObjectFormat::Sha1,
11944                            "1111111111111111111111111111111111111111",
11945                        )
11946                        .expect("test operation should succeed")
11947                    ],
11948                    ..ReceivePackRequest::default()
11949                },
11950                push_options: None,
11951                packfile: Vec::new(),
11952            })
11953            .is_err()
11954        );
11955    }
11956
11957    #[test]
11958    fn receive_pack_report_status_parses_and_encodes_status_lines() {
11959        let frames = vec![
11960            PktLineFrame::Data(b"unpack ok\n".to_vec()),
11961            PktLineFrame::Data(b"ok refs/heads/main\n".to_vec()),
11962            PktLineFrame::Data(b"ng refs/heads/old non-fast-forward\n".to_vec()),
11963            PktLineFrame::Flush,
11964        ];
11965        let report =
11966            parse_receive_pack_report_status(&frames).expect("test operation should succeed");
11967        assert_eq!(
11968            report,
11969            ReceivePackReportStatus {
11970                unpack: ReceivePackUnpackStatus::Ok,
11971                commands: vec![
11972                    ReceivePackCommandStatus::Ok {
11973                        name: "refs/heads/main".into(),
11974                    },
11975                    ReceivePackCommandStatus::Ng {
11976                        name: "refs/heads/old".into(),
11977                        message: "non-fast-forward".into(),
11978                    },
11979                ],
11980            }
11981        );
11982        assert_eq!(
11983            encode_receive_pack_report_status(&report).expect("test operation should succeed"),
11984            frames
11985        );
11986
11987        let frames = vec![
11988            PktLineFrame::Data(b"unpack pack exceeds maximum size\n".to_vec()),
11989            PktLineFrame::Flush,
11990        ];
11991        assert_eq!(
11992            parse_receive_pack_report_status(&frames).expect("test operation should succeed"),
11993            ReceivePackReportStatus {
11994                unpack: ReceivePackUnpackStatus::Error("pack exceeds maximum size".into()),
11995                commands: Vec::new(),
11996            }
11997        );
11998    }
11999
12000    #[test]
12001    fn receive_pack_report_status_streams_round_trip() {
12002        let report = ReceivePackReportStatus {
12003            unpack: ReceivePackUnpackStatus::Ok,
12004            commands: vec![ReceivePackCommandStatus::Ok {
12005                name: "refs/heads/main".into(),
12006            }],
12007        };
12008        let mut encoded = Vec::new();
12009        write_receive_pack_report_status(&mut encoded, &report)
12010            .expect("test operation should succeed");
12011        encoded.extend_from_slice(b"tail");
12012
12013        let mut input = encoded.as_slice();
12014        assert_eq!(
12015            read_receive_pack_report_status(&mut input).expect("test operation should succeed"),
12016            report
12017        );
12018        assert_eq!(input, b"tail");
12019    }
12020
12021    #[test]
12022    fn receive_pack_report_status_rejects_malformed_status_lines() {
12023        assert!(parse_receive_pack_report_status(&[]).is_err());
12024        assert!(
12025            parse_receive_pack_report_status(&[
12026                PktLineFrame::Data(b"unpack ok\n".to_vec()),
12027                PktLineFrame::Data(b"ok refs/heads/main\n".to_vec()),
12028            ])
12029            .is_err()
12030        );
12031        assert!(
12032            parse_receive_pack_report_status(&[
12033                PktLineFrame::Flush,
12034                PktLineFrame::Data(b"ok refs/heads/main\n".to_vec()),
12035            ])
12036            .is_err()
12037        );
12038        assert!(
12039            parse_receive_pack_report_status(&[
12040                PktLineFrame::Data(b"unpack ok\n".to_vec()),
12041                PktLineFrame::Data(b"bad refs/heads/main\n".to_vec()),
12042                PktLineFrame::Flush,
12043            ])
12044            .is_err()
12045        );
12046        assert!(
12047            parse_receive_pack_report_status(&[
12048                PktLineFrame::Data(b"unpack ok\n".to_vec()),
12049                PktLineFrame::Data(b"ng refs/heads/main\n".to_vec()),
12050                PktLineFrame::Flush,
12051            ])
12052            .is_err()
12053        );
12054        assert!(
12055            encode_receive_pack_report_status(&ReceivePackReportStatus {
12056                unpack: ReceivePackUnpackStatus::Error("".into()),
12057                commands: Vec::new(),
12058            })
12059            .is_err()
12060        );
12061        assert!(
12062            encode_receive_pack_report_status(&ReceivePackReportStatus {
12063                unpack: ReceivePackUnpackStatus::Ok,
12064                commands: vec![ReceivePackCommandStatus::Ok {
12065                    name: "bad ref".into(),
12066                }],
12067            })
12068            .is_err()
12069        );
12070    }
12071
12072    #[test]
12073    fn receive_pack_report_status_v2_parses_and_encodes_options() {
12074        let old_oid = ObjectId::from_hex(
12075            ObjectFormat::Sha1,
12076            "1111111111111111111111111111111111111111",
12077        )
12078        .expect("test operation should succeed");
12079        let new_oid = ObjectId::from_hex(
12080            ObjectFormat::Sha1,
12081            "2222222222222222222222222222222222222222",
12082        )
12083        .expect("test operation should succeed");
12084        let frames = vec![
12085            PktLineFrame::Data(b"unpack ok\n".to_vec()),
12086            PktLineFrame::Data(b"ok refs/for/main\n".to_vec()),
12087            PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12088            PktLineFrame::Data(
12089                b"option old-oid 1111111111111111111111111111111111111111\n".to_vec(),
12090            ),
12091            PktLineFrame::Data(
12092                b"option new-oid 2222222222222222222222222222222222222222\n".to_vec(),
12093            ),
12094            PktLineFrame::Data(b"option forced-update\n".to_vec()),
12095            PktLineFrame::Data(b"ng refs/heads/old rejected by hook\n".to_vec()),
12096            PktLineFrame::Flush,
12097        ];
12098        let report = parse_receive_pack_report_status_v2(ObjectFormat::Sha1, &frames)
12099            .expect("test operation should succeed");
12100        assert_eq!(
12101            report,
12102            ReceivePackReportStatusV2 {
12103                unpack: ReceivePackUnpackStatus::Ok,
12104                commands: vec![
12105                    ReceivePackCommandStatusV2::Ok {
12106                        name: "refs/for/main".into(),
12107                        options: ReceivePackCommandStatusV2Options {
12108                            refname: Some("refs/heads/main".into()),
12109                            old_oid: Some(old_oid),
12110                            new_oid: Some(new_oid),
12111                            forced_update: true,
12112                        },
12113                    },
12114                    ReceivePackCommandStatusV2::Ng {
12115                        name: "refs/heads/old".into(),
12116                        message: "rejected by hook".into(),
12117                    },
12118                ],
12119            }
12120        );
12121        assert_eq!(
12122            encode_receive_pack_report_status_v2(&report).expect("test operation should succeed"),
12123            frames
12124        );
12125    }
12126
12127    #[test]
12128    fn receive_pack_report_status_v2_streams_round_trip() {
12129        let report = ReceivePackReportStatusV2 {
12130            unpack: ReceivePackUnpackStatus::Ok,
12131            commands: vec![ReceivePackCommandStatusV2::Ok {
12132                name: "refs/for/main".into(),
12133                options: ReceivePackCommandStatusV2Options {
12134                    refname: Some("refs/heads/main".into()),
12135                    old_oid: None,
12136                    new_oid: None,
12137                    forced_update: false,
12138                },
12139            }],
12140        };
12141        let mut encoded = Vec::new();
12142        write_receive_pack_report_status_v2(&mut encoded, &report)
12143            .expect("test operation should succeed");
12144        encoded.extend_from_slice(b"tail");
12145
12146        let mut input = encoded.as_slice();
12147        assert_eq!(
12148            read_receive_pack_report_status_v2(ObjectFormat::Sha1, &mut input)
12149                .expect("test operation should succeed"),
12150            report
12151        );
12152        assert_eq!(input, b"tail");
12153    }
12154
12155    #[test]
12156    fn receive_pack_report_status_v2_rejects_malformed_options() {
12157        assert!(parse_receive_pack_report_status_v2(ObjectFormat::Sha1, &[]).is_err());
12158        assert!(
12159            parse_receive_pack_report_status_v2(
12160                ObjectFormat::Sha1,
12161                &[
12162                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12163                    PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12164                    PktLineFrame::Flush,
12165                ],
12166            )
12167            .is_err()
12168        );
12169        assert!(
12170            parse_receive_pack_report_status_v2(
12171                ObjectFormat::Sha1,
12172                &[
12173                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12174                    PktLineFrame::Data(b"ng refs/heads/main rejected\n".to_vec()),
12175                    PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12176                    PktLineFrame::Flush,
12177                ],
12178            )
12179            .is_err()
12180        );
12181        assert!(
12182            parse_receive_pack_report_status_v2(
12183                ObjectFormat::Sha1,
12184                &[
12185                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12186                    PktLineFrame::Data(b"ok refs/for/main\n".to_vec()),
12187                    PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12188                    PktLineFrame::Data(b"option refname refs/heads/next\n".to_vec()),
12189                    PktLineFrame::Flush,
12190                ],
12191            )
12192            .is_err()
12193        );
12194        assert!(
12195            parse_receive_pack_report_status_v2(
12196                ObjectFormat::Sha1,
12197                &[
12198                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12199                    PktLineFrame::Data(b"ok refs/for/main\n".to_vec()),
12200                    PktLineFrame::Data(b"option old-oid not-an-oid\n".to_vec()),
12201                    PktLineFrame::Flush,
12202                ],
12203            )
12204            .is_err()
12205        );
12206        assert!(
12207            encode_receive_pack_report_status_v2(&ReceivePackReportStatusV2 {
12208                unpack: ReceivePackUnpackStatus::Ok,
12209                commands: vec![ReceivePackCommandStatusV2::Ok {
12210                    name: "refs/for/main".into(),
12211                    options: ReceivePackCommandStatusV2Options {
12212                        refname: Some("bad ref".into()),
12213                        ..ReceivePackCommandStatusV2Options::default()
12214                    },
12215                }],
12216            })
12217            .is_err()
12218        );
12219    }
12220
12221    #[test]
12222    fn receive_pack_push_options_parse_and_encode_options() {
12223        let frames = vec![
12224            PktLineFrame::Data(b"ci.skip\n".to_vec()),
12225            PktLineFrame::Data(b"deploy target=staging\n".to_vec()),
12226            PktLineFrame::Data(b"\n".to_vec()),
12227            PktLineFrame::Flush,
12228        ];
12229        let options =
12230            parse_receive_pack_push_options(&frames).expect("test operation should succeed");
12231        assert_eq!(
12232            options,
12233            vec![
12234                "ci.skip".to_string(),
12235                "deploy target=staging".to_string(),
12236                String::new(),
12237            ]
12238        );
12239        assert_eq!(
12240            encode_receive_pack_push_options(&options).expect("test operation should succeed"),
12241            frames
12242        );
12243        assert_eq!(
12244            parse_receive_pack_push_options(&[PktLineFrame::Flush])
12245                .expect("test operation should succeed"),
12246            Vec::<String>::new()
12247        );
12248    }
12249
12250    #[test]
12251    fn receive_pack_push_options_streams_round_trip() {
12252        let options = vec!["ci.skip".to_string(), "reviewer=alice".to_string()];
12253        let mut encoded = Vec::new();
12254        write_receive_pack_push_options(&mut encoded, &options)
12255            .expect("test operation should succeed");
12256        encoded.extend_from_slice(b"PACK");
12257
12258        let mut input = encoded.as_slice();
12259        assert_eq!(
12260            read_receive_pack_push_options(&mut input).expect("test operation should succeed"),
12261            options
12262        );
12263        assert_eq!(input, b"PACK");
12264    }
12265
12266    #[test]
12267    fn receive_pack_push_options_reject_malformed_streams() {
12268        assert!(
12269            parse_receive_pack_push_options(&[PktLineFrame::Data(b"ci.skip\n".to_vec())]).is_err()
12270        );
12271        assert!(
12272            parse_receive_pack_push_options(&[PktLineFrame::Delimiter, PktLineFrame::Flush])
12273                .is_err()
12274        );
12275        assert!(
12276            parse_receive_pack_push_options(&[
12277                PktLineFrame::Data(b"ci.skip\n".to_vec()),
12278                PktLineFrame::Flush,
12279                PktLineFrame::Data(b"after\n".to_vec()),
12280            ])
12281            .is_err()
12282        );
12283        assert!(
12284            parse_receive_pack_push_options(&[
12285                PktLineFrame::Data(b"bad\0option\n".to_vec()),
12286                PktLineFrame::Flush,
12287            ])
12288            .is_err()
12289        );
12290        assert!(encode_receive_pack_push_options(&["bad\noption".to_string()]).is_err());
12291    }
12292
12293    #[test]
12294    fn protocol_v2_advertisement_parses_version_and_capabilities() {
12295        let frames = parse_pkt_line_stream(
12296            b"000eversion 2\n0015agent=git/2.54.0\n0013ls-refs=unborn\n0027fetch=shallow wait-for-done filter\n0012server-option\n0000",
12297        )
12298        .expect("test operation should succeed");
12299        let handshake =
12300            parse_protocol_v2_advertisement(&frames).expect("test operation should succeed");
12301        assert_eq!(handshake.protocol, ProtocolVersion::V2);
12302        assert_eq!(
12303            handshake.capabilities,
12304            vec![
12305                Capability {
12306                    name: "agent".into(),
12307                    value: Some("git/2.54.0".into()),
12308                },
12309                Capability {
12310                    name: "ls-refs".into(),
12311                    value: Some("unborn".into()),
12312                },
12313                Capability {
12314                    name: "fetch".into(),
12315                    value: Some("shallow wait-for-done filter".into()),
12316                },
12317                Capability {
12318                    name: "server-option".into(),
12319                    value: None,
12320                },
12321            ]
12322        );
12323        assert_eq!(
12324            encode_protocol_v2_advertisement(&handshake).expect("test operation should succeed"),
12325            frames
12326        );
12327    }
12328
12329    #[test]
12330    fn protocol_v2_advertisement_reads_until_flush() {
12331        let mut input = b"000eversion 2\n0013ls-refs=unborn\n0000next-session".as_slice();
12332        let handshake =
12333            read_protocol_v2_advertisement(&mut input).expect("test operation should succeed");
12334        assert_eq!(handshake.protocol, ProtocolVersion::V2);
12335        assert_eq!(
12336            handshake.capabilities,
12337            vec![Capability {
12338                name: "ls-refs".into(),
12339                value: Some("unborn".into()),
12340            }]
12341        );
12342        assert_eq!(input, b"next-session");
12343    }
12344
12345    #[test]
12346    fn protocol_v2_advertisement_writes_stream() {
12347        let handshake = TransportHandshake {
12348            protocol: ProtocolVersion::V2,
12349            capabilities: vec![
12350                Capability {
12351                    name: "agent".into(),
12352                    value: Some("sley/0".into()),
12353                },
12354                Capability {
12355                    name: "fetch".into(),
12356                    value: Some("shallow filter".into()),
12357                },
12358            ],
12359        };
12360        let mut encoded = Vec::new();
12361        write_protocol_v2_advertisement(&mut encoded, &handshake)
12362            .expect("test operation should succeed");
12363        let mut input = encoded.as_slice();
12364        assert_eq!(
12365            read_protocol_v2_advertisement(&mut input).expect("test operation should succeed"),
12366            handshake
12367        );
12368        assert!(input.is_empty());
12369        assert!(
12370            encode_protocol_v2_advertisement(&TransportHandshake {
12371                protocol: ProtocolVersion::V1,
12372                capabilities: Vec::new(),
12373            })
12374            .is_err()
12375        );
12376    }
12377
12378    #[test]
12379    fn protocol_v2_advertisement_rejects_malformed_sequences() {
12380        assert!(parse_protocol_v2_advertisement(&[]).is_err());
12381        assert!(
12382            parse_protocol_v2_advertisement(&[
12383                PktLineFrame::Data(b"version 1\n".to_vec()),
12384                PktLineFrame::Flush,
12385            ])
12386            .is_err()
12387        );
12388        assert!(
12389            parse_protocol_v2_advertisement(&[PktLineFrame::Data(b"version 2\n".to_vec())])
12390                .is_err()
12391        );
12392        assert!(
12393            parse_protocol_v2_advertisement(&[
12394                PktLineFrame::Data(b"version 2\n".to_vec()),
12395                PktLineFrame::Delimiter,
12396            ])
12397            .is_err()
12398        );
12399        assert!(
12400            parse_protocol_v2_advertisement(&[
12401                PktLineFrame::Data(b"version 2\n".to_vec()),
12402                PktLineFrame::Data(b"fetch=\n".to_vec()),
12403                PktLineFrame::Flush,
12404            ])
12405            .is_err()
12406        );
12407    }
12408
12409    #[test]
12410    fn protocol_v2_command_request_parses_and_encodes_sections() {
12411        let frames = parse_pkt_line_stream(
12412            b"0014command=ls-refs\n0011agent=sley/0\n0017object-format=sha1\n00010009peel\n000csymrefs\n001bref-prefix refs/heads/\n0000",
12413        )
12414        .expect("test operation should succeed");
12415        let request =
12416            parse_protocol_v2_command_request(&frames).expect("test operation should succeed");
12417        assert_eq!(
12418            request,
12419            ProtocolV2CommandRequest {
12420                command: "ls-refs".into(),
12421                capabilities: vec![
12422                    Capability {
12423                        name: "agent".into(),
12424                        value: Some("sley/0".into()),
12425                    },
12426                    Capability {
12427                        name: "object-format".into(),
12428                        value: Some("sha1".into()),
12429                    },
12430                ],
12431                arguments: vec![
12432                    b"peel".to_vec(),
12433                    b"symrefs".to_vec(),
12434                    b"ref-prefix refs/heads/".to_vec(),
12435                ],
12436            }
12437        );
12438        assert_eq!(
12439            encode_protocol_v2_command_request(&request).expect("test operation should succeed"),
12440            frames
12441        );
12442    }
12443
12444    #[test]
12445    fn protocol_v2_command_request_allows_no_argument_section() {
12446        let frames = parse_pkt_line_stream(b"0012command=fetch\n0000")
12447            .expect("test operation should succeed");
12448        let request =
12449            parse_protocol_v2_command_request(&frames).expect("test operation should succeed");
12450        assert_eq!(
12451            request,
12452            ProtocolV2CommandRequest {
12453                command: "fetch".into(),
12454                capabilities: Vec::new(),
12455                arguments: Vec::new(),
12456            }
12457        );
12458        assert_eq!(
12459            encode_protocol_v2_command_request(&request).expect("test operation should succeed"),
12460            frames
12461        );
12462    }
12463
12464    #[test]
12465    fn protocol_v2_request_parses_commands_and_empty_done() {
12466        let frames = parse_pkt_line_stream(b"0012command=fetch\n0000")
12467            .expect("test operation should succeed");
12468        let command = ProtocolV2CommandRequest {
12469            command: "fetch".into(),
12470            capabilities: Vec::new(),
12471            arguments: Vec::new(),
12472        };
12473        assert_eq!(
12474            parse_protocol_v2_request(&frames).expect("test operation should succeed"),
12475            ProtocolV2Request::Command(command.clone())
12476        );
12477        assert_eq!(
12478            encode_protocol_v2_request(&ProtocolV2Request::Command(command))
12479                .expect("test operation should succeed"),
12480            frames
12481        );
12482
12483        assert_eq!(
12484            parse_protocol_v2_request(&[PktLineFrame::Flush])
12485                .expect("test operation should succeed"),
12486            ProtocolV2Request::Done
12487        );
12488        assert_eq!(
12489            encode_protocol_v2_request(&ProtocolV2Request::Done)
12490                .expect("test operation should succeed"),
12491            vec![PktLineFrame::Flush]
12492        );
12493    }
12494
12495    #[test]
12496    fn protocol_v2_request_streams_empty_done() {
12497        let mut encoded = Vec::new();
12498        write_protocol_v2_request(&mut encoded, &ProtocolV2Request::Done)
12499            .expect("test operation should succeed");
12500        encoded.extend_from_slice(b"tail");
12501
12502        let mut input = encoded.as_slice();
12503        assert_eq!(
12504            read_protocol_v2_request(&mut input).expect("test operation should succeed"),
12505            ProtocolV2Request::Done
12506        );
12507        assert_eq!(input, b"tail");
12508        let mut command_input = encoded.as_slice();
12509        assert!(read_protocol_v2_command_request(&mut command_input).is_err());
12510    }
12511
12512    #[test]
12513    fn protocol_v2_command_request_streams_round_trip() {
12514        let request = ProtocolV2CommandRequest {
12515            command: "ls-refs".into(),
12516            capabilities: vec![Capability {
12517                name: "agent".into(),
12518                value: Some("sley/0".into()),
12519            }],
12520            arguments: vec![b"peel".to_vec(), b"symrefs".to_vec()],
12521        };
12522        let mut encoded = Vec::new();
12523        write_protocol_v2_command_request(&mut encoded, &request)
12524            .expect("test operation should succeed");
12525        encoded.extend_from_slice(b"tail");
12526
12527        let mut input = encoded.as_slice();
12528        assert_eq!(
12529            read_protocol_v2_command_request(&mut input).expect("test operation should succeed"),
12530            request
12531        );
12532        assert_eq!(input, b"tail");
12533    }
12534
12535    #[test]
12536    fn protocol_v2_command_request_rejects_malformed_sequences() {
12537        assert!(parse_protocol_v2_command_request(&[]).is_err());
12538        assert!(
12539            parse_protocol_v2_command_request(&[
12540                PktLineFrame::Data(b"agent=sley/0\n".to_vec()),
12541                PktLineFrame::Flush,
12542            ])
12543            .is_err()
12544        );
12545        assert!(
12546            parse_protocol_v2_command_request(&[
12547                PktLineFrame::Data(b"command=ls-refs\n".to_vec()),
12548                PktLineFrame::Delimiter,
12549                PktLineFrame::Delimiter,
12550                PktLineFrame::Flush,
12551            ])
12552            .is_err()
12553        );
12554        assert!(
12555            parse_protocol_v2_command_request(&[
12556                PktLineFrame::Data(b"command=ls-refs\n".to_vec()),
12557                PktLineFrame::Delimiter,
12558                PktLineFrame::Data(b"\n".to_vec()),
12559                PktLineFrame::Flush,
12560            ])
12561            .is_err()
12562        );
12563        assert!(
12564            encode_protocol_v2_command_request(&ProtocolV2CommandRequest {
12565                command: "bad command".into(),
12566                capabilities: Vec::new(),
12567                arguments: Vec::new(),
12568            })
12569            .is_err()
12570        );
12571    }
12572
12573    #[test]
12574    fn protocol_v2_ls_refs_request_parses_and_encodes_arguments() {
12575        let command = ProtocolV2CommandRequest {
12576            command: "ls-refs".into(),
12577            capabilities: Vec::new(),
12578            arguments: vec![
12579                b"peel".to_vec(),
12580                b"symrefs".to_vec(),
12581                b"unborn".to_vec(),
12582                b"ref-prefix HEAD".to_vec(),
12583                b"ref-prefix refs/heads/".to_vec(),
12584            ],
12585        };
12586        let request = ProtocolV2LsRefsRequest::from_command_request(&command)
12587            .expect("test operation should succeed");
12588        assert_eq!(
12589            request,
12590            ProtocolV2LsRefsRequest {
12591                peel: true,
12592                symrefs: true,
12593                unborn: true,
12594                ref_prefixes: vec!["HEAD".into(), "refs/heads/".into()],
12595            }
12596        );
12597        assert_eq!(
12598            request
12599                .to_command_request()
12600                .expect("test operation should succeed"),
12601            command
12602        );
12603        assert!(
12604            ProtocolV2LsRefsRequest::from_command_request(&ProtocolV2CommandRequest {
12605                command: "fetch".into(),
12606                capabilities: Vec::new(),
12607                arguments: Vec::new(),
12608            })
12609            .is_err()
12610        );
12611        assert!(
12612            ProtocolV2LsRefsRequest::from_command_request(&ProtocolV2CommandRequest {
12613                command: "ls-refs".into(),
12614                capabilities: Vec::new(),
12615                arguments: vec![b"ref-prefix ".to_vec()],
12616            })
12617            .is_err()
12618        );
12619    }
12620
12621    #[test]
12622    fn protocol_v2_ls_refs_request_streams_round_trip() {
12623        let request = ProtocolV2LsRefsRequest {
12624            peel: true,
12625            symrefs: true,
12626            unborn: false,
12627            ref_prefixes: vec!["HEAD".into(), "refs/tags/".into()],
12628        };
12629        let mut encoded = Vec::new();
12630        write_protocol_v2_ls_refs_request(&mut encoded, &request)
12631            .expect("test operation should succeed");
12632        encoded.extend_from_slice(b"tail");
12633
12634        let mut input = encoded.as_slice();
12635        assert_eq!(
12636            read_protocol_v2_ls_refs_request(&mut input).expect("test operation should succeed"),
12637            request
12638        );
12639        assert_eq!(input, b"tail");
12640    }
12641
12642    #[test]
12643    fn protocol_v2_ls_refs_response_parses_and_encodes_records() {
12644        let oid = ObjectId::from_hex(
12645            ObjectFormat::Sha1,
12646            "1111111111111111111111111111111111111111",
12647        )
12648        .expect("test operation should succeed");
12649        let peeled = ObjectId::from_hex(
12650            ObjectFormat::Sha1,
12651            "2222222222222222222222222222222222222222",
12652        )
12653        .expect("test operation should succeed");
12654        let frames = vec![
12655            PktLineFrame::Data(
12656                b"1111111111111111111111111111111111111111 refs/tags/v1 peeled:2222222222222222222222222222222222222222 symref-target:refs/heads/main custom\n"
12657                    .to_vec(),
12658            ),
12659            PktLineFrame::Data(b"unborn HEAD symref-target:refs/heads/main\n".to_vec()),
12660            PktLineFrame::Flush,
12661        ];
12662        let records = parse_protocol_v2_ls_refs_response(ObjectFormat::Sha1, &frames)
12663            .expect("test operation should succeed");
12664        assert_eq!(
12665            records,
12666            vec![
12667                ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
12668                    oid,
12669                    name: "refs/tags/v1".into(),
12670                    peeled: Some(peeled),
12671                    symref_target: Some("refs/heads/main".into()),
12672                    attributes: vec!["custom".into()],
12673                }),
12674                ProtocolV2LsRefsRecord::Unborn {
12675                    name: "HEAD".into(),
12676                    symref_target: Some("refs/heads/main".into()),
12677                    attributes: Vec::new(),
12678                },
12679            ]
12680        );
12681        assert_eq!(
12682            encode_protocol_v2_ls_refs_response(&records).expect("test operation should succeed"),
12683            frames
12684        );
12685    }
12686
12687    #[test]
12688    fn protocol_v2_ls_refs_response_streams_round_trip() {
12689        let oid = ObjectId::from_hex(
12690            ObjectFormat::Sha1,
12691            "1111111111111111111111111111111111111111",
12692        )
12693        .expect("test operation should succeed");
12694        let records = vec![ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
12695            oid,
12696            name: "refs/heads/main".into(),
12697            peeled: None,
12698            symref_target: Some("refs/heads/trunk".into()),
12699            attributes: vec!["custom".into()],
12700        })];
12701        let mut encoded = Vec::new();
12702        write_protocol_v2_ls_refs_response(&mut encoded, &records)
12703            .expect("test operation should succeed");
12704        encoded.extend_from_slice(b"tail");
12705
12706        let mut input = encoded.as_slice();
12707        assert_eq!(
12708            read_protocol_v2_ls_refs_response(ObjectFormat::Sha1, &mut input)
12709                .expect("test operation should succeed"),
12710            records
12711        );
12712        assert_eq!(input, b"tail");
12713    }
12714
12715    #[test]
12716    fn protocol_v2_ls_refs_response_reads_stateless_response_end() {
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: None,
12727            attributes: Vec::new(),
12728        })];
12729        let mut encoded = Vec::new();
12730        write_protocol_v2_ls_refs_response_with_response_end(&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_until_response_end(ObjectFormat::Sha1, &mut input)
12737                .expect("test operation should succeed"),
12738            records
12739        );
12740        assert_eq!(input, b"tail");
12741        assert!(
12742            parse_protocol_v2_ls_refs_response(
12743                ObjectFormat::Sha1,
12744                &[
12745                    PktLineFrame::Data(
12746                        b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec()
12747                    ),
12748                    PktLineFrame::ResponseEnd
12749                ],
12750            )
12751            .is_err()
12752        );
12753    }
12754
12755    #[test]
12756    fn protocol_v2_ls_refs_exchange_writes_request_and_reads_response() {
12757        let oid = ObjectId::from_hex(
12758            ObjectFormat::Sha1,
12759            "1111111111111111111111111111111111111111",
12760        )
12761        .expect("test operation should succeed");
12762        let request = ProtocolV2LsRefsRequest {
12763            peel: true,
12764            symrefs: true,
12765            unborn: false,
12766            ref_prefixes: vec!["refs/heads/".into()],
12767        };
12768        let records = vec![ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
12769            oid,
12770            name: "refs/heads/main".into(),
12771            peeled: None,
12772            symref_target: None,
12773            attributes: Vec::new(),
12774        })];
12775        let mut response = Vec::new();
12776        write_protocol_v2_ls_refs_response(&mut response, &records)
12777            .expect("test operation should succeed");
12778
12779        let mut input = response.as_slice();
12780        let mut output = Vec::new();
12781        assert_eq!(
12782            exchange_protocol_v2_ls_refs(ObjectFormat::Sha1, &mut input, &mut output, &request)
12783                .expect("test operation should succeed"),
12784            records
12785        );
12786        assert!(input.is_empty());
12787        let mut output_read = output.as_slice();
12788        assert_eq!(
12789            read_protocol_v2_ls_refs_request(&mut output_read)
12790                .expect("test operation should succeed"),
12791            request
12792        );
12793    }
12794
12795    #[test]
12796    fn protocol_v2_ls_refs_response_rejects_malformed_records() {
12797        assert!(
12798            parse_protocol_v2_ls_refs_response(
12799                ObjectFormat::Sha1,
12800                &[PktLineFrame::Data(
12801                    b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec()
12802                )],
12803            )
12804            .is_err()
12805        );
12806        assert!(
12807            parse_protocol_v2_ls_refs_response(
12808                ObjectFormat::Sha1,
12809                &[
12810                    PktLineFrame::Data(
12811                        b"1111111111111111111111111111111111111111 refs/heads/main peeled:2222222222222222222222222222222222222222 peeled:3333333333333333333333333333333333333333\n"
12812                            .to_vec()
12813                    ),
12814                    PktLineFrame::Flush,
12815                ],
12816            )
12817            .is_err()
12818        );
12819        assert!(
12820            parse_protocol_v2_ls_refs_response(
12821                ObjectFormat::Sha1,
12822                &[
12823                    PktLineFrame::Data(
12824                        b"unborn HEAD peeled:2222222222222222222222222222222222222222\n".to_vec()
12825                    ),
12826                    PktLineFrame::Flush,
12827                ],
12828            )
12829            .is_err()
12830        );
12831        assert!(
12832            encode_protocol_v2_ls_refs_response(&[ProtocolV2LsRefsRecord::Ref(
12833                ProtocolV2LsRefsRef {
12834                    oid: ObjectId::from_hex(
12835                        ObjectFormat::Sha1,
12836                        "1111111111111111111111111111111111111111",
12837                    )
12838                    .expect("test operation should succeed"),
12839                    name: "refs/heads/main".into(),
12840                    peeled: None,
12841                    symref_target: None,
12842                    attributes: vec!["peeled:2222222222222222222222222222222222222222".into()],
12843                }
12844            )])
12845            .is_err()
12846        );
12847    }
12848
12849    #[test]
12850    fn protocol_v2_fetch_request_parses_and_encodes_arguments() {
12851        let want = ObjectId::from_hex(
12852            ObjectFormat::Sha1,
12853            "1111111111111111111111111111111111111111",
12854        )
12855        .expect("test operation should succeed");
12856        let have = ObjectId::from_hex(
12857            ObjectFormat::Sha1,
12858            "2222222222222222222222222222222222222222",
12859        )
12860        .expect("test operation should succeed");
12861        let shallow = ObjectId::from_hex(
12862            ObjectFormat::Sha1,
12863            "3333333333333333333333333333333333333333",
12864        )
12865        .expect("test operation should succeed");
12866        let command = ProtocolV2CommandRequest {
12867            command: "fetch".into(),
12868            capabilities: Vec::new(),
12869            arguments: vec![
12870                b"want 1111111111111111111111111111111111111111".to_vec(),
12871                b"want-ref refs/heads/main".to_vec(),
12872                b"have 2222222222222222222222222222222222222222".to_vec(),
12873                b"shallow 3333333333333333333333333333333333333333".to_vec(),
12874                b"deepen 10".to_vec(),
12875                b"deepen-since 123456789".to_vec(),
12876                b"deepen-not refs/tags/v1".to_vec(),
12877                b"deepen-relative".to_vec(),
12878                b"filter blob:none".to_vec(),
12879                b"packfile-uris http,https".to_vec(),
12880                b"thin-pack".to_vec(),
12881                b"no-progress".to_vec(),
12882                b"include-tag".to_vec(),
12883                b"ofs-delta".to_vec(),
12884                b"sideband-all".to_vec(),
12885                b"wait-for-done".to_vec(),
12886                b"done".to_vec(),
12887            ],
12888        };
12889        let request = ProtocolV2FetchRequest::from_command_request(ObjectFormat::Sha1, &command)
12890            .expect("test operation should succeed");
12891        assert_eq!(
12892            request,
12893            ProtocolV2FetchRequest {
12894                wants: vec![want],
12895                want_refs: vec!["refs/heads/main".into()],
12896                haves: vec![have],
12897                shallow: vec![shallow],
12898                deepen: Some(10),
12899                deepen_since: Some(123456789),
12900                deepen_not: vec!["refs/tags/v1".into()],
12901                deepen_relative: true,
12902                filter: Some("blob:none".into()),
12903                packfile_uris: Some("http,https".into()),
12904                thin_pack: true,
12905                no_progress: true,
12906                include_tag: true,
12907                ofs_delta: true,
12908                sideband_all: true,
12909                wait_for_done: true,
12910                done: true,
12911            }
12912        );
12913        assert_eq!(
12914            request
12915                .to_command_request()
12916                .expect("test operation should succeed"),
12917            command
12918        );
12919    }
12920
12921    #[test]
12922    fn protocol_v2_fetch_request_rejects_malformed_arguments() {
12923        assert!(
12924            ProtocolV2FetchRequest::from_command_request(
12925                ObjectFormat::Sha1,
12926                &ProtocolV2CommandRequest {
12927                    command: "ls-refs".into(),
12928                    capabilities: Vec::new(),
12929                    arguments: Vec::new(),
12930                },
12931            )
12932            .is_err()
12933        );
12934        assert!(
12935            ProtocolV2FetchRequest::from_command_request(
12936                ObjectFormat::Sha1,
12937                &ProtocolV2CommandRequest {
12938                    command: "fetch".into(),
12939                    capabilities: Vec::new(),
12940                    arguments: vec![b"want not-an-oid".to_vec()],
12941                },
12942            )
12943            .is_err()
12944        );
12945        assert!(
12946            ProtocolV2FetchRequest::from_command_request(
12947                ObjectFormat::Sha1,
12948                &ProtocolV2CommandRequest {
12949                    command: "fetch".into(),
12950                    capabilities: Vec::new(),
12951                    arguments: vec![b"deepen 0".to_vec()],
12952                },
12953            )
12954            .is_err()
12955        );
12956        assert!(
12957            ProtocolV2FetchRequest::from_command_request(
12958                ObjectFormat::Sha1,
12959                &ProtocolV2CommandRequest {
12960                    command: "fetch".into(),
12961                    capabilities: Vec::new(),
12962                    arguments: vec![b"filter blob:none".to_vec(), b"filter tree:0".to_vec()],
12963                },
12964            )
12965            .is_err()
12966        );
12967        assert!(
12968            ProtocolV2FetchRequest {
12969                deepen: Some(0),
12970                ..ProtocolV2FetchRequest::default()
12971            }
12972            .to_command_request()
12973            .is_err()
12974        );
12975    }
12976
12977    #[test]
12978    fn protocol_v2_fetch_request_streams_round_trip() {
12979        let want = ObjectId::from_hex(
12980            ObjectFormat::Sha1,
12981            "1111111111111111111111111111111111111111",
12982        )
12983        .expect("test operation should succeed");
12984        let have = ObjectId::from_hex(
12985            ObjectFormat::Sha1,
12986            "2222222222222222222222222222222222222222",
12987        )
12988        .expect("test operation should succeed");
12989        let request = ProtocolV2FetchRequest {
12990            wants: vec![want],
12991            haves: vec![have],
12992            deepen: Some(5),
12993            filter: Some("blob:none".into()),
12994            thin_pack: true,
12995            done: true,
12996            ..ProtocolV2FetchRequest::default()
12997        };
12998        let mut encoded = Vec::new();
12999        write_protocol_v2_fetch_request(&mut encoded, &request)
13000            .expect("test operation should succeed");
13001        encoded.extend_from_slice(b"tail");
13002
13003        let mut input = encoded.as_slice();
13004        assert_eq!(
13005            read_protocol_v2_fetch_request(ObjectFormat::Sha1, &mut input)
13006                .expect("test operation should succeed"),
13007            request
13008        );
13009        assert_eq!(input, b"tail");
13010    }
13011
13012    #[test]
13013    fn protocol_v2_fetch_response_parses_and_encodes_sections() {
13014        let ack = ObjectId::from_hex(
13015            ObjectFormat::Sha1,
13016            "1111111111111111111111111111111111111111",
13017        )
13018        .expect("test operation should succeed");
13019        let shallow = ObjectId::from_hex(
13020            ObjectFormat::Sha1,
13021            "2222222222222222222222222222222222222222",
13022        )
13023        .expect("test operation should succeed");
13024        let wanted = ObjectId::from_hex(
13025            ObjectFormat::Sha1,
13026            "3333333333333333333333333333333333333333",
13027        )
13028        .expect("test operation should succeed");
13029        let pack_hash = ObjectId::from_hex(
13030            ObjectFormat::Sha1,
13031            "4444444444444444444444444444444444444444",
13032        )
13033        .expect("test operation should succeed");
13034        let frames = vec![
13035            PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13036            PktLineFrame::Data(b"ACK 1111111111111111111111111111111111111111\n".to_vec()),
13037            PktLineFrame::Data(b"ready\n".to_vec()),
13038            PktLineFrame::Delimiter,
13039            PktLineFrame::Data(b"shallow-info\n".to_vec()),
13040            PktLineFrame::Data(b"shallow 2222222222222222222222222222222222222222\n".to_vec()),
13041            PktLineFrame::Delimiter,
13042            PktLineFrame::Data(b"wanted-refs\n".to_vec()),
13043            PktLineFrame::Data(
13044                b"3333333333333333333333333333333333333333 refs/heads/main\n".to_vec(),
13045            ),
13046            PktLineFrame::Delimiter,
13047            PktLineFrame::Data(b"packfile-uris\n".to_vec()),
13048            PktLineFrame::Data(
13049                b"4444444444444444444444444444444444444444 https://example.invalid/pack-a.pack\n"
13050                    .to_vec(),
13051            ),
13052            PktLineFrame::Delimiter,
13053            PktLineFrame::Data(b"packfile\n".to_vec()),
13054            PktLineFrame::Data(b"\x01PACK bytes".to_vec()),
13055            PktLineFrame::Flush,
13056        ];
13057        let sections = parse_protocol_v2_fetch_response(ObjectFormat::Sha1, &frames)
13058            .expect("test operation should succeed");
13059        assert_eq!(
13060            sections,
13061            vec![
13062                ProtocolV2FetchResponseSection::Acknowledgments(vec![
13063                    ProtocolV2FetchAcknowledgment::Ack(ack),
13064                    ProtocolV2FetchAcknowledgment::Ready,
13065                ]),
13066                ProtocolV2FetchResponseSection::ShallowInfo(vec![
13067                    ProtocolV2FetchShallowInfo::Shallow(shallow)
13068                ]),
13069                ProtocolV2FetchResponseSection::WantedRefs(vec![ProtocolV2FetchWantedRef {
13070                    oid: wanted,
13071                    name: "refs/heads/main".into(),
13072                }]),
13073                ProtocolV2FetchResponseSection::PackfileUris(vec![ProtocolV2FetchPackfileUri {
13074                    pack_hash,
13075                    uri: "https://example.invalid/pack-a.pack".into(),
13076                }]),
13077                ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK bytes".to_vec()]),
13078            ]
13079        );
13080        assert_eq!(
13081            encode_protocol_v2_fetch_response(&sections).expect("test operation should succeed"),
13082            frames
13083        );
13084    }
13085
13086    #[test]
13087    fn protocol_v2_fetch_response_preserves_unknown_sections() {
13088        let frames = vec![
13089            PktLineFrame::Data(b"server-feature\n".to_vec()),
13090            PktLineFrame::Data(b"opaque line\n".to_vec()),
13091            PktLineFrame::Flush,
13092        ];
13093        let sections = parse_protocol_v2_fetch_response(ObjectFormat::Sha1, &frames)
13094            .expect("test operation should succeed");
13095        assert_eq!(
13096            sections,
13097            vec![ProtocolV2FetchResponseSection::Unknown {
13098                name: "server-feature".into(),
13099                lines: vec![b"opaque line\n".to_vec()],
13100            }]
13101        );
13102        assert_eq!(
13103            encode_protocol_v2_fetch_response(&sections).expect("test operation should succeed"),
13104            frames
13105        );
13106    }
13107
13108    #[test]
13109    fn protocol_v2_fetch_response_streams_round_trip() {
13110        let ack = ObjectId::from_hex(
13111            ObjectFormat::Sha1,
13112            "1111111111111111111111111111111111111111",
13113        )
13114        .expect("test operation should succeed");
13115        let sections = vec![
13116            ProtocolV2FetchResponseSection::Acknowledgments(vec![
13117                ProtocolV2FetchAcknowledgment::Ack(ack),
13118                ProtocolV2FetchAcknowledgment::Ready,
13119            ]),
13120            ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK bytes".to_vec()]),
13121        ];
13122        let mut encoded = Vec::new();
13123        write_protocol_v2_fetch_response(&mut encoded, &sections)
13124            .expect("test operation should succeed");
13125        encoded.extend_from_slice(b"tail");
13126
13127        let mut input = encoded.as_slice();
13128        assert_eq!(
13129            read_protocol_v2_fetch_response(ObjectFormat::Sha1, &mut input)
13130                .expect("test operation should succeed"),
13131            sections
13132        );
13133        assert_eq!(input, b"tail");
13134    }
13135
13136    #[test]
13137    fn protocol_v2_fetch_sideband_all_response_parses_sections_and_progress() {
13138        let frames = vec![
13139            PktLineFrame::Data(
13140                encode_sideband_packet(&SideBandPacket {
13141                    channel: SideBandChannel::Data,
13142                    data: b"acknowledgments\n".to_vec(),
13143                })
13144                .expect("test operation should succeed"),
13145            ),
13146            PktLineFrame::Data(
13147                encode_sideband_packet(&SideBandPacket {
13148                    channel: SideBandChannel::Data,
13149                    data: b"NAK\n".to_vec(),
13150                })
13151                .expect("test operation should succeed"),
13152            ),
13153            PktLineFrame::Data(
13154                encode_sideband_packet(&SideBandPacket {
13155                    channel: SideBandChannel::Progress,
13156                    data: b"keepalive\n".to_vec(),
13157                })
13158                .expect("test operation should succeed"),
13159            ),
13160            PktLineFrame::Delimiter,
13161            PktLineFrame::Data(
13162                encode_sideband_packet(&SideBandPacket {
13163                    channel: SideBandChannel::Data,
13164                    data: b"packfile\n".to_vec(),
13165                })
13166                .expect("test operation should succeed"),
13167            ),
13168            PktLineFrame::Data(b"\x01PACK".to_vec()),
13169            PktLineFrame::Data(b"\x02counting objects\n".to_vec()),
13170            PktLineFrame::Flush,
13171        ];
13172
13173        let response = parse_protocol_v2_fetch_sideband_all_response(ObjectFormat::Sha1, &frames)
13174            .expect("test operation should succeed");
13175        assert_eq!(
13176            response,
13177            ProtocolV2FetchSidebandAllResponse {
13178                sections: vec![
13179                    ProtocolV2FetchResponseSection::Acknowledgments(vec![
13180                        ProtocolV2FetchAcknowledgment::Nak
13181                    ]),
13182                    ProtocolV2FetchResponseSection::Packfile(vec![
13183                        b"\x01PACK".to_vec(),
13184                        b"\x02counting objects\n".to_vec(),
13185                    ]),
13186                ],
13187                progress: vec![b"keepalive\n".to_vec()],
13188            }
13189        );
13190        assert_eq!(
13191            demux_protocol_v2_fetch_packfile(&response.sections)
13192                .expect("test operation should succeed"),
13193            Some(SideBandDemux {
13194                data: b"PACK".to_vec(),
13195                progress: vec![b"counting objects\n".to_vec()],
13196            })
13197        );
13198    }
13199
13200    #[test]
13201    fn protocol_v2_fetch_sideband_all_response_streams_round_trip() {
13202        let sections = vec![
13203            ProtocolV2FetchResponseSection::Acknowledgments(vec![
13204                ProtocolV2FetchAcknowledgment::Nak,
13205            ]),
13206            ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK bytes".to_vec()]),
13207        ];
13208        let mut encoded = Vec::new();
13209        write_protocol_v2_fetch_sideband_all_response(&mut encoded, &sections)
13210            .expect("test operation should succeed");
13211        encoded.extend_from_slice(b"tail");
13212
13213        let mut input = encoded.as_slice();
13214        assert_eq!(
13215            read_protocol_v2_fetch_sideband_all_response(ObjectFormat::Sha1, &mut input)
13216                .expect("test operation should succeed"),
13217            ProtocolV2FetchSidebandAllResponse {
13218                sections: sections.clone(),
13219                progress: Vec::new(),
13220            }
13221        );
13222        assert_eq!(input, b"tail");
13223
13224        let mut encoded = Vec::new();
13225        write_protocol_v2_fetch_sideband_all_response_with_response_end(&mut encoded, &sections)
13226            .expect("test operation should succeed");
13227        encoded.extend_from_slice(b"tail");
13228
13229        let mut input = encoded.as_slice();
13230        assert_eq!(
13231            read_protocol_v2_fetch_sideband_all_response_until_response_end(
13232                ObjectFormat::Sha1,
13233                &mut input,
13234            )
13235            .expect("test operation should succeed")
13236            .sections,
13237            sections
13238        );
13239        assert_eq!(input, b"tail");
13240    }
13241
13242    #[test]
13243    fn protocol_v2_fetch_sideband_all_response_rejects_malformed_sideband() {
13244        assert!(
13245            parse_protocol_v2_fetch_sideband_all_response(
13246                ObjectFormat::Sha1,
13247                &[
13248                    PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13249                    PktLineFrame::Flush,
13250                ],
13251            )
13252            .is_err()
13253        );
13254        assert!(
13255            parse_protocol_v2_fetch_sideband_all_response(
13256                ObjectFormat::Sha1,
13257                &[
13258                    PktLineFrame::Data(
13259                        encode_sideband_packet(&SideBandPacket {
13260                            channel: SideBandChannel::Fatal,
13261                            data: b"remote died\n".to_vec(),
13262                        })
13263                        .expect("test operation should succeed"),
13264                    ),
13265                    PktLineFrame::Flush,
13266                ],
13267            )
13268            .is_err()
13269        );
13270    }
13271
13272    #[test]
13273    fn protocol_v2_object_info_response_parses_and_encodes_size_records() {
13274        let oid = ObjectId::from_hex(
13275            ObjectFormat::Sha1,
13276            "1111111111111111111111111111111111111111",
13277        )
13278        .expect("test operation should succeed");
13279        let frames = vec![
13280            PktLineFrame::Data(b"size\n".to_vec()),
13281            PktLineFrame::Data(b"1111111111111111111111111111111111111111 12345\n".to_vec()),
13282            PktLineFrame::Flush,
13283        ];
13284        let response = parse_protocol_v2_object_info_response(ObjectFormat::Sha1, &frames)
13285            .expect("test operation should succeed");
13286        assert_eq!(
13287            response,
13288            ProtocolV2ObjectInfoResponse {
13289                size: true,
13290                records: vec![ProtocolV2ObjectInfoRecord { oid, size: 12345 }],
13291            }
13292        );
13293        assert_eq!(
13294            encode_protocol_v2_object_info_response(&response)
13295                .expect("test operation should succeed"),
13296            frames
13297        );
13298    }
13299
13300    #[test]
13301    fn protocol_v2_object_info_response_streams_and_exchanges() {
13302        let request = ProtocolV2ObjectInfoRequest {
13303            size: true,
13304            oids: vec![
13305                ObjectId::from_hex(
13306                    ObjectFormat::Sha1,
13307                    "1111111111111111111111111111111111111111",
13308                )
13309                .expect("test operation should succeed"),
13310            ],
13311        };
13312        let response = ProtocolV2ObjectInfoResponse {
13313            size: true,
13314            records: vec![ProtocolV2ObjectInfoRecord {
13315                oid: request.oids[0].clone(),
13316                size: 7,
13317            }],
13318        };
13319
13320        let mut encoded = Vec::new();
13321        write_protocol_v2_object_info_response(&mut encoded, &response)
13322            .expect("test operation should succeed");
13323        encoded.extend_from_slice(b"tail");
13324        let mut input = encoded.as_slice();
13325        assert_eq!(
13326            read_protocol_v2_object_info_response(ObjectFormat::Sha1, &mut input)
13327                .expect("test operation should succeed"),
13328            response
13329        );
13330        assert_eq!(input, b"tail");
13331
13332        let mut response_bytes = Vec::new();
13333        write_protocol_v2_object_info_response(&mut response_bytes, &response)
13334            .expect("test operation should succeed");
13335        let mut input = response_bytes.as_slice();
13336        let mut output = Vec::new();
13337        assert_eq!(
13338            exchange_protocol_v2_object_info(
13339                ObjectFormat::Sha1,
13340                &mut input,
13341                &mut output,
13342                &request,
13343            )
13344            .expect("test operation should succeed"),
13345            response
13346        );
13347        assert!(input.is_empty());
13348        let mut output_read = output.as_slice();
13349        assert_eq!(
13350            read_protocol_v2_object_info_request(ObjectFormat::Sha1, &mut output_read)
13351                .expect("test operation should succeed"),
13352            request
13353        );
13354    }
13355
13356    #[test]
13357    fn protocol_v2_object_info_response_rejects_malformed_records() {
13358        assert!(parse_protocol_v2_object_info_response(ObjectFormat::Sha1, &[]).is_err());
13359        assert!(
13360            parse_protocol_v2_object_info_response(
13361                ObjectFormat::Sha1,
13362                &[PktLineFrame::Data(b"size\n".to_vec())],
13363            )
13364            .is_err()
13365        );
13366        assert!(
13367            parse_protocol_v2_object_info_response(
13368                ObjectFormat::Sha1,
13369                &[PktLineFrame::Data(b"type\n".to_vec()), PktLineFrame::Flush,],
13370            )
13371            .is_err()
13372        );
13373        assert!(
13374            parse_protocol_v2_object_info_response(
13375                ObjectFormat::Sha1,
13376                &[
13377                    PktLineFrame::Data(b"size\n".to_vec()),
13378                    PktLineFrame::Data(
13379                        b"1111111111111111111111111111111111111111 not-a-size\n".to_vec()
13380                    ),
13381                    PktLineFrame::Flush,
13382                ],
13383            )
13384            .is_err()
13385        );
13386        assert!(
13387            parse_protocol_v2_object_info_response(
13388                ObjectFormat::Sha1,
13389                &[
13390                    PktLineFrame::Data(b"size\n".to_vec()),
13391                    PktLineFrame::Delimiter,
13392                    PktLineFrame::Flush,
13393                ],
13394            )
13395            .is_err()
13396        );
13397        assert!(
13398            encode_protocol_v2_object_info_response(&ProtocolV2ObjectInfoResponse {
13399                size: false,
13400                records: Vec::new(),
13401            })
13402            .is_err()
13403        );
13404    }
13405
13406    #[test]
13407    fn protocol_v2_fetch_response_reads_stateless_response_end() {
13408        let sections = vec![ProtocolV2FetchResponseSection::Acknowledgments(vec![
13409            ProtocolV2FetchAcknowledgment::Nak,
13410        ])];
13411        let mut encoded = Vec::new();
13412        write_protocol_v2_fetch_response_with_response_end(&mut encoded, &sections)
13413            .expect("test operation should succeed");
13414        encoded.extend_from_slice(b"tail");
13415
13416        let mut input = encoded.as_slice();
13417        assert_eq!(
13418            read_protocol_v2_fetch_response_until_response_end(ObjectFormat::Sha1, &mut input)
13419                .expect("test operation should succeed"),
13420            sections
13421        );
13422        assert_eq!(input, b"tail");
13423        assert!(
13424            parse_protocol_v2_fetch_response(
13425                ObjectFormat::Sha1,
13426                &[
13427                    PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13428                    PktLineFrame::ResponseEnd,
13429                ],
13430            )
13431            .is_err()
13432        );
13433    }
13434
13435    #[test]
13436    fn protocol_v2_fetch_exchange_writes_request_and_reads_response() {
13437        let want = ObjectId::from_hex(
13438            ObjectFormat::Sha1,
13439            "1111111111111111111111111111111111111111",
13440        )
13441        .expect("test operation should succeed");
13442        let request = ProtocolV2FetchRequest {
13443            wants: vec![want],
13444            thin_pack: true,
13445            done: true,
13446            ..ProtocolV2FetchRequest::default()
13447        };
13448        let sections = vec![ProtocolV2FetchResponseSection::Acknowledgments(vec![
13449            ProtocolV2FetchAcknowledgment::Nak,
13450        ])];
13451        let mut response = Vec::new();
13452        write_protocol_v2_fetch_response(&mut response, &sections)
13453            .expect("test operation should succeed");
13454
13455        let mut input = response.as_slice();
13456        let mut output = Vec::new();
13457        assert_eq!(
13458            exchange_protocol_v2_fetch(ObjectFormat::Sha1, &mut input, &mut output, &request)
13459                .expect("test operation should succeed"),
13460            sections
13461        );
13462        assert!(input.is_empty());
13463        let mut output_read = output.as_slice();
13464        assert_eq!(
13465            read_protocol_v2_fetch_request(ObjectFormat::Sha1, &mut output_read)
13466                .expect("test operation should succeed"),
13467            request
13468        );
13469    }
13470
13471    #[test]
13472    fn protocol_v2_fetch_packfile_demuxes_sideband_section() {
13473        let sections = vec![
13474            ProtocolV2FetchResponseSection::Acknowledgments(vec![
13475                ProtocolV2FetchAcknowledgment::Nak,
13476            ]),
13477            ProtocolV2FetchResponseSection::Packfile(vec![
13478                b"\x01PACK".to_vec(),
13479                b"\x02counting objects\n".to_vec(),
13480                b"\x01 bytes".to_vec(),
13481                b"\x02done\n".to_vec(),
13482            ]),
13483        ];
13484
13485        assert_eq!(
13486            demux_protocol_v2_fetch_packfile(&sections).expect("test operation should succeed"),
13487            Some(SideBandDemux {
13488                data: b"PACK bytes".to_vec(),
13489                progress: vec![b"counting objects\n".to_vec(), b"done\n".to_vec()],
13490            })
13491        );
13492        assert_eq!(
13493            demux_protocol_v2_fetch_packfile(&[ProtocolV2FetchResponseSection::Acknowledgments(
13494                vec![ProtocolV2FetchAcknowledgment::Nak],
13495            )])
13496            .expect("test operation should succeed"),
13497            None
13498        );
13499    }
13500
13501    #[test]
13502    fn protocol_v2_fetch_packfile_demux_rejects_duplicate_or_bad_sideband() {
13503        assert!(
13504            demux_protocol_v2_fetch_packfile(&[
13505                ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK".to_vec()]),
13506                ProtocolV2FetchResponseSection::Packfile(vec![b"\x01more".to_vec()]),
13507            ])
13508            .is_err()
13509        );
13510        assert!(
13511            demux_protocol_v2_fetch_packfile(&[ProtocolV2FetchResponseSection::Packfile(vec![
13512                b"\x03remote died\n".to_vec()
13513            ])])
13514            .is_err()
13515        );
13516        assert!(
13517            demux_protocol_v2_fetch_packfile(&[ProtocolV2FetchResponseSection::Packfile(vec![
13518                b"\x04bad".to_vec()
13519            ])])
13520            .is_err()
13521        );
13522    }
13523
13524    #[test]
13525    fn protocol_v2_fetch_response_rejects_malformed_sections() {
13526        assert!(
13527            parse_protocol_v2_fetch_response(
13528                ObjectFormat::Sha1,
13529                &[PktLineFrame::Data(b"acknowledgments\n".to_vec())],
13530            )
13531            .is_err()
13532        );
13533        assert!(
13534            parse_protocol_v2_fetch_response(
13535                ObjectFormat::Sha1,
13536                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
13537            )
13538            .is_err()
13539        );
13540        assert!(
13541            parse_protocol_v2_fetch_response(
13542                ObjectFormat::Sha1,
13543                &[
13544                    PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13545                    PktLineFrame::Data(b"ACK not-an-oid\n".to_vec()),
13546                    PktLineFrame::Flush,
13547                ],
13548            )
13549            .is_err()
13550        );
13551        assert!(
13552            parse_protocol_v2_fetch_response(
13553                ObjectFormat::Sha1,
13554                &[
13555                    PktLineFrame::Data(b"packfile-uris\n".to_vec()),
13556                    PktLineFrame::Data(b"https://example.invalid/pack-a.pack\n".to_vec()),
13557                    PktLineFrame::Flush,
13558                ],
13559            )
13560            .is_err()
13561        );
13562        assert!(
13563            parse_protocol_v2_fetch_response(
13564                ObjectFormat::Sha1,
13565                &[
13566                    PktLineFrame::Data(b"packfile-uris\n".to_vec()),
13567                    PktLineFrame::Data(
13568                        b"not-a-hash https://example.invalid/pack-a.pack\n".to_vec()
13569                    ),
13570                    PktLineFrame::Flush,
13571                ],
13572            )
13573            .is_err()
13574        );
13575        assert!(
13576            encode_protocol_v2_fetch_response(&[ProtocolV2FetchResponseSection::WantedRefs(vec![
13577                ProtocolV2FetchWantedRef {
13578                    oid: ObjectId::from_hex(
13579                        ObjectFormat::Sha1,
13580                        "1111111111111111111111111111111111111111",
13581                    )
13582                    .expect("test operation should succeed"),
13583                    name: "bad ref".into(),
13584                }
13585            ])])
13586            .is_err()
13587        );
13588    }
13589
13590    #[test]
13591    fn protocol_v2_ls_refs_response_bridges_into_ref_advertisement_set() {
13592        let head = ObjectId::from_hex(
13593            ObjectFormat::Sha1,
13594            "1111111111111111111111111111111111111111",
13595        )
13596        .expect("test operation should succeed");
13597        let tag = ObjectId::from_hex(
13598            ObjectFormat::Sha1,
13599            "2222222222222222222222222222222222222222",
13600        )
13601        .expect("test operation should succeed");
13602        let tag_peeled = ObjectId::from_hex(
13603            ObjectFormat::Sha1,
13604            "3333333333333333333333333333333333333333",
13605        )
13606        .expect("test operation should succeed");
13607        let frames = vec![
13608            PktLineFrame::Data(
13609                b"1111111111111111111111111111111111111111 HEAD symref-target:refs/heads/main\n"
13610                    .to_vec(),
13611            ),
13612            PktLineFrame::Data(
13613                b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec(),
13614            ),
13615            PktLineFrame::Data(
13616                b"2222222222222222222222222222222222222222 refs/tags/v1 peeled:3333333333333333333333333333333333333333\n"
13617                    .to_vec(),
13618            ),
13619            PktLineFrame::Flush,
13620        ];
13621
13622        let set = parse_protocol_v2_ls_refs_response_as_ref_advertisement_set(
13623            ObjectFormat::Sha1,
13624            &frames,
13625        )
13626        .expect("test operation should succeed");
13627        assert_eq!(
13628            set,
13629            RefAdvertisementSet {
13630                protocol: ProtocolVersion::V2,
13631                refs: vec![
13632                    RefAdvertisement {
13633                        oid: head.clone(),
13634                        name: "HEAD".into(),
13635                        capabilities: vec![Capability {
13636                            name: "symref".into(),
13637                            value: Some("HEAD:refs/heads/main".into()),
13638                        }],
13639                    },
13640                    RefAdvertisement {
13641                        oid: head,
13642                        name: "refs/heads/main".into(),
13643                        capabilities: Vec::new(),
13644                    },
13645                    RefAdvertisement {
13646                        oid: tag,
13647                        name: "refs/tags/v1".into(),
13648                        capabilities: Vec::new(),
13649                    },
13650                    RefAdvertisement {
13651                        oid: tag_peeled,
13652                        name: "refs/tags/v1^{}".into(),
13653                        capabilities: Vec::new(),
13654                    },
13655                ],
13656                shallow: Vec::new(),
13657            }
13658        );
13659
13660        // The streaming reader path produces the same bridged set.
13661        let mut encoded = Vec::new();
13662        write_pkt_line_frames(&mut encoded, &frames).expect("test operation should succeed");
13663        encoded.extend_from_slice(b"tail");
13664        let mut input = encoded.as_slice();
13665        assert_eq!(
13666            read_protocol_v2_ls_refs_response_as_ref_advertisement_set(
13667                ObjectFormat::Sha1,
13668                &mut input,
13669            )
13670            .expect("test operation should succeed"),
13671            set,
13672        );
13673        assert_eq!(input, b"tail");
13674    }
13675
13676    #[test]
13677    fn protocol_v2_ls_refs_records_bridge_unborn_head_symref_and_empty() {
13678        // An unborn HEAD pointing at an as-yet-uncreated branch carries only a
13679        // symref capability and has no concrete ref to attach it to.
13680        let records = vec![ProtocolV2LsRefsRecord::Unborn {
13681            name: "HEAD".into(),
13682            symref_target: Some("refs/heads/main".into()),
13683            attributes: Vec::new(),
13684        }];
13685        assert!(protocol_v2_ls_refs_records_to_ref_advertisement_set(&records).is_err());
13686
13687        // An empty ls-refs response bridges to an empty v2 set.
13688        assert_eq!(
13689            protocol_v2_ls_refs_records_to_ref_advertisement_set(&[])
13690                .expect("test operation should succeed"),
13691            RefAdvertisementSet {
13692                protocol: ProtocolVersion::V2,
13693                refs: Vec::new(),
13694                shallow: Vec::new(),
13695            }
13696        );
13697
13698        // An unborn HEAD alongside a concrete ref attaches the symref to the
13699        // first ref, matching the v0/v1 advertisement convention.
13700        let main = ObjectId::from_hex(
13701            ObjectFormat::Sha1,
13702            "4444444444444444444444444444444444444444",
13703        )
13704        .expect("test operation should succeed");
13705        let records = vec![
13706            ProtocolV2LsRefsRecord::Unborn {
13707                name: "HEAD".into(),
13708                symref_target: Some("refs/heads/main".into()),
13709                attributes: Vec::new(),
13710            },
13711            ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
13712                oid: main.clone(),
13713                name: "refs/heads/main".into(),
13714                peeled: None,
13715                symref_target: None,
13716                attributes: Vec::new(),
13717            }),
13718        ];
13719        let set = protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
13720            .expect("test operation should succeed");
13721        assert_eq!(
13722            set,
13723            RefAdvertisementSet {
13724                protocol: ProtocolVersion::V2,
13725                refs: vec![RefAdvertisement {
13726                    oid: main,
13727                    name: "refs/heads/main".into(),
13728                    capabilities: vec![Capability {
13729                        name: "symref".into(),
13730                        value: Some("HEAD:refs/heads/main".into()),
13731                    }],
13732                }],
13733                shallow: Vec::new(),
13734            }
13735        );
13736    }
13737}