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};
7use std::sync::RwLock;
8
9// ---------------------------------------------------------------------------
10// GIT_TRACE_PACKET: emit a human-readable trace of pkt-line traffic, matching
11// git's `pkt-line.c::packet_trace`. The format is
12// `packet: %12s%c <escaped-data>` where `%12s` is the program identity
13// (right-aligned in 12 columns), `%c` is `>` for writes and `<` for reads, and
14// non-printable bytes are octal-escaped (`\0` for NUL) with newlines suppressed.
15// Tracing is gated by the `GIT_TRACE_PACKET` env var so the (hot) pkt-line
16// paths early-out to a single env probe when tracing is off.
17// ---------------------------------------------------------------------------
18
19/// The program identity shown in `packet: %12s` (e.g. `git`, `ls-remote`,
20/// `clone`, `fetch`). Mirrors git's `packet_trace_identity`. Defaults to `git`.
21static PACKET_TRACE_IDENTITY: RwLock<Option<String>> = RwLock::new(None);
22
23/// Set the program identity used in subsequent packet traces (the CLI sets this
24/// from the running subcommand). Mirrors `packet_trace_identity(prog)`.
25pub fn set_packet_trace_identity(prog: &str) {
26    if let Ok(mut guard) = PACKET_TRACE_IDENTITY.write() {
27        *guard = Some(prog.to_string());
28    }
29}
30
31fn packet_trace_prefix() -> String {
32    PACKET_TRACE_IDENTITY
33        .read()
34        .ok()
35        .and_then(|guard| guard.clone())
36        .unwrap_or_else(|| "git".to_string())
37}
38
39/// The destination for `GIT_TRACE_PACKET`: `1`/`2`/`true` → stderr, an absolute
40/// path is opened append+create, `0`/`false`/empty/unset disables. Mirrors
41/// git's `get_trace_fd` for the values the tests use.
42fn packet_trace_sink() -> Option<Box<dyn Write>> {
43    let value = std::env::var("GIT_TRACE_PACKET").ok()?;
44    let lower = value.to_ascii_lowercase();
45    match lower.as_str() {
46        "" | "0" | "false" => None,
47        "1" | "2" | "true" => Some(Box::new(std::io::stderr())),
48        _ => {
49            if std::path::Path::new(&value).is_absolute() {
50                std::fs::OpenOptions::new()
51                    .create(true)
52                    .append(true)
53                    .open(&value)
54                    .ok()
55                    .map(|f| Box::new(f) as Box<dyn Write>)
56            } else {
57                None
58            }
59        }
60    }
61}
62
63/// Whether packet tracing is enabled (cheap env probe for the hot-path guard).
64fn packet_trace_enabled() -> bool {
65    match std::env::var("GIT_TRACE_PACKET") {
66        Ok(value) => {
67            let lower = value.to_ascii_lowercase();
68            !matches!(lower.as_str(), "" | "0" | "false")
69        }
70        Err(_) => false,
71    }
72}
73
74/// Emit one packet-trace line for `data` (one pkt-line's framing token or
75/// payload). `is_write` selects the `>`/`<` direction. Octal-escapes
76/// non-printable bytes and suppresses newlines, exactly like git's
77/// `packet_trace`. The pack-data stream is collapsed to `PACK ...` on its first
78/// chunk (git does the same so the trace stays human-readable).
79fn packet_trace(data: &[u8], is_write: bool) {
80    if !packet_trace_enabled() {
81        return;
82    }
83    let Some(mut sink) = packet_trace_sink() else {
84        return;
85    };
86    // Collapse pack data: once a payload starts with "PACK" (raw) or "\1PACK"
87    // (sideband channel 1), git emits a single `PACK ...` marker for the human
88    // trace rather than the binary stream. We approximate per-call (stateless):
89    // any payload beginning with those magic bytes is rendered as `PACK ...`.
90    let rendered: Vec<u8> = if data.starts_with(b"PACK") || data.starts_with(b"\x01PACK") {
91        b"PACK ...".to_vec()
92    } else {
93        data.to_vec()
94    };
95
96    let mut out = format!("packet: {:>12}{} ", packet_trace_prefix(), if is_write {
97        '>'
98    } else {
99        '<'
100    });
101    for &byte in &rendered {
102        if byte == b'\n' {
103            continue;
104        }
105        if (0x20..=0x7e).contains(&byte) {
106            out.push(byte as char);
107        } else {
108            out.push_str(&format!("\\{byte:o}"));
109        }
110    }
111    out.push('\n');
112    let _ = sink.write_all(out.as_bytes());
113    let _ = sink.flush();
114}
115
116/// Trace a frame on the wire. Flush/delim/response-end map to their 4-byte
117/// tokens (`0000`/`0001`/`0002`) like git, data frames to their payload.
118fn packet_trace_frame(frame: &PktLineFrame, is_write: bool) {
119    if !packet_trace_enabled() {
120        return;
121    }
122    match frame {
123        PktLineFrame::Data(payload) => packet_trace(payload, is_write),
124        PktLineFrame::Flush => packet_trace(b"0000", is_write),
125        PktLineFrame::Delimiter => packet_trace(b"0001", is_write),
126        PktLineFrame::ResponseEnd => packet_trace(b"0002", is_write),
127    }
128}
129
130pub const PKT_LINE_MAX_LEN: usize = 65_520;
131
132pub const PKT_LINE_MAX_PAYLOAD_LEN: usize = PKT_LINE_MAX_LEN - 4;
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub enum ProtocolVersion {
136    V0,
137    V1,
138    V2,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct PktLine(pub Vec<u8>);
143
144impl PktLine {
145    pub fn encode(&self) -> Vec<u8> {
146        encode_pkt_line_payload(&self.0)
147    }
148
149    pub fn try_encode(&self) -> Result<Vec<u8>> {
150        validate_pkt_line_payload(&self.0)?;
151        Ok(self.encode())
152    }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub enum PktLineFrame {
157    Data(Vec<u8>),
158    Flush,
159    Delimiter,
160    ResponseEnd,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct ProtocolErrorLine {
165    pub message: String,
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum GitService {
170    UploadPack,
171    ReceivePack,
172    UploadArchive,
173}
174
175impl GitService {
176    pub fn as_str(self) -> &'static str {
177        match self {
178            Self::UploadPack => "git-upload-pack",
179            Self::ReceivePack => "git-receive-pack",
180            Self::UploadArchive => "git-upload-archive",
181        }
182    }
183}
184
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct RefSpec {
187    pub force: bool,
188    pub negative: bool,
189    pub src: Option<String>,
190    pub dst: Option<String>,
191    pub pattern: bool,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct FetchHeadRecord {
196    pub oid: ObjectId,
197    pub not_for_merge: bool,
198    pub description: String,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct FetchRefUpdate {
203    pub src: String,
204    pub dst: Option<String>,
205    pub oid: ObjectId,
206    pub not_for_merge: bool,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct PushSourceRef {
211    pub name: String,
212    pub oid: ObjectId,
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum SideBandChannel {
217    Data,
218    Progress,
219    Fatal,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
223pub struct SideBandPacket {
224    pub channel: SideBandChannel,
225    pub data: Vec<u8>,
226}
227
228#[derive(Debug, Clone, PartialEq, Eq, Default)]
229pub struct SideBandDemux {
230    pub data: Vec<u8>,
231    pub progress: Vec<Vec<u8>>,
232}
233
234#[derive(Debug, Clone, PartialEq, Eq, Default)]
235pub struct UploadArchiveRequest {
236    pub arguments: Vec<String>,
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
240pub enum UploadArchiveResponse {
241    Ack { sideband: Vec<SideBandPacket> },
242    Nack { message: String },
243}
244
245impl PktLineFrame {
246    pub fn data(payload: impl Into<Vec<u8>>) -> Result<Self> {
247        let payload = payload.into();
248        validate_pkt_line_payload(&payload)?;
249        Ok(Self::Data(payload))
250    }
251
252    pub fn encode(&self) -> Vec<u8> {
253        match self {
254            Self::Data(payload) => encode_pkt_line_payload(payload),
255            Self::Flush => b"0000".to_vec(),
256            Self::Delimiter => b"0001".to_vec(),
257            Self::ResponseEnd => b"0002".to_vec(),
258        }
259    }
260
261    pub fn try_encode(&self) -> Result<Vec<u8>> {
262        match self {
263            Self::Data(payload) => try_encode_pkt_line_payload(payload),
264            Self::Flush | Self::Delimiter | Self::ResponseEnd => Ok(self.encode()),
265        }
266    }
267
268    pub fn parse(input: &[u8]) -> Result<(Self, usize)> {
269        if input.len() < 4 {
270            return Err(GitError::InvalidFormat("truncated pkt-line length".into()));
271        }
272        let len = parse_pkt_len(&input[..4])?;
273        match len {
274            0 => Ok((Self::Flush, 4)),
275            1 => Ok((Self::Delimiter, 4)),
276            2 => Ok((Self::ResponseEnd, 4)),
277            3 => Err(GitError::InvalidFormat(
278                "reserved pkt-line length 0003".into(),
279            )),
280            4..=PKT_LINE_MAX_LEN => {
281                if input.len() < len {
282                    return Err(GitError::InvalidFormat(format!(
283                        "truncated pkt-line payload: expected {} bytes, got {}",
284                        len - 4,
285                        input.len().saturating_sub(4)
286                    )));
287                }
288                Ok((Self::Data(input[4..len].to_vec()), len))
289            }
290            _ => Err(GitError::InvalidFormat(format!(
291                "pkt-line length exceeds {PKT_LINE_MAX_LEN}: {len}"
292            ))),
293        }
294    }
295}
296
297fn validate_pkt_line_payload(payload: &[u8]) -> Result<()> {
298    if payload.len() > PKT_LINE_MAX_PAYLOAD_LEN {
299        return Err(GitError::InvalidFormat(format!(
300            "pkt-line payload exceeds {PKT_LINE_MAX_PAYLOAD_LEN} bytes"
301        )));
302    }
303    Ok(())
304}
305
306fn pkt_line_header(len: usize) -> [u8; 4] {
307    const HEX: &[u8; 16] = b"0123456789abcdef";
308    [
309        HEX[(len >> 12) & 0xf],
310        HEX[(len >> 8) & 0xf],
311        HEX[(len >> 4) & 0xf],
312        HEX[len & 0xf],
313    ]
314}
315
316fn encode_pkt_line_payload(payload: &[u8]) -> Vec<u8> {
317    let len = payload.len() + 4;
318    let mut out = Vec::with_capacity(len);
319    out.extend_from_slice(&pkt_line_header(len));
320    out.extend_from_slice(payload);
321    out
322}
323
324fn try_encode_pkt_line_payload(payload: &[u8]) -> Result<Vec<u8>> {
325    validate_pkt_line_payload(payload)?;
326    Ok(encode_pkt_line_payload(payload))
327}
328
329pub fn parse_pkt_line_stream(mut input: &[u8]) -> Result<Vec<PktLineFrame>> {
330    let mut frames = Vec::new();
331    while !input.is_empty() {
332        let (frame, consumed) = PktLineFrame::parse(input)?;
333        frames.push(frame);
334        input = &input[consumed..];
335    }
336    Ok(frames)
337}
338
339fn parse_pkt_line_frames_until_flush_from(mut input: &[u8]) -> Result<(Vec<PktLineFrame>, usize)> {
340    let mut frames = Vec::new();
341    let mut total = 0usize;
342    loop {
343        if input.is_empty() {
344            return Err(GitError::InvalidFormat(
345                "pkt-line stream ended before flush".into(),
346            ));
347        }
348        let (frame, consumed) = PktLineFrame::parse(input)?;
349        total += consumed;
350        let done = matches!(frame, PktLineFrame::Flush);
351        frames.push(frame);
352        input = &input[consumed..];
353        if done {
354            return Ok((frames, total));
355        }
356    }
357}
358
359pub fn read_pkt_line_frame(reader: &mut impl Read) -> Result<Option<PktLineFrame>> {
360    let mut header = [0u8; 4];
361    let mut read = 0usize;
362    while read < header.len() {
363        match reader.read(&mut header[read..]) {
364            Ok(0) if read == 0 => return Ok(None),
365            Ok(0) => {
366                return Err(GitError::InvalidFormat("truncated pkt-line length".into()));
367            }
368            Ok(n) => read += n,
369            Err(err) if err.kind() == ErrorKind::Interrupted => {}
370            Err(err) => return Err(err.into()),
371        }
372    }
373
374    let len = parse_pkt_len(&header)?;
375    let frame = match len {
376        0 => PktLineFrame::Flush,
377        1 => PktLineFrame::Delimiter,
378        2 => PktLineFrame::ResponseEnd,
379        3 => {
380            return Err(GitError::InvalidFormat(
381                "reserved pkt-line length 0003".into(),
382            ));
383        }
384        4..=PKT_LINE_MAX_LEN => {
385            let mut payload = vec![0; len - 4];
386            reader.read_exact(&mut payload)?;
387            PktLineFrame::Data(payload)
388        }
389        _ => {
390            return Err(GitError::InvalidFormat(format!(
391                "pkt-line length exceeds {PKT_LINE_MAX_LEN}: {len}"
392            )));
393        }
394    };
395    packet_trace_frame(&frame, false);
396    Ok(Some(frame))
397}
398
399pub fn read_pkt_line_frames(reader: &mut impl Read) -> Result<Vec<PktLineFrame>> {
400    let mut frames = Vec::new();
401    while let Some(frame) = read_pkt_line_frame(reader)? {
402        frames.push(frame);
403    }
404    Ok(frames)
405}
406
407pub fn read_pkt_line_frames_until_flush(reader: &mut impl Read) -> Result<Vec<PktLineFrame>> {
408    read_pkt_line_frames_until_control(reader, |frame| matches!(frame, PktLineFrame::Flush))
409}
410
411pub fn read_pkt_line_frames_until_response_end(
412    reader: &mut impl Read,
413) -> Result<Vec<PktLineFrame>> {
414    read_pkt_line_frames_until_control(reader, |frame| matches!(frame, PktLineFrame::ResponseEnd))
415}
416
417fn read_pkt_line_frames_until_control(
418    reader: &mut impl Read,
419    stop: impl Fn(&PktLineFrame) -> bool,
420) -> Result<Vec<PktLineFrame>> {
421    let mut frames = Vec::new();
422    loop {
423        let Some(frame) = read_pkt_line_frame(reader)? else {
424            return Err(GitError::InvalidFormat(
425                "pkt-line stream ended before control packet".into(),
426            ));
427        };
428        let done = stop(&frame);
429        frames.push(frame);
430        if done {
431            return Ok(frames);
432        }
433    }
434}
435
436pub fn write_pkt_line_frame(writer: &mut impl Write, frame: &PktLineFrame) -> Result<()> {
437    match frame {
438        // `write_pkt_line_payload` already traces the data line.
439        PktLineFrame::Data(payload) => write_pkt_line_payload(writer, payload)?,
440        PktLineFrame::Flush => {
441            packet_trace(b"0000", true);
442            writer.write_all(b"0000")?;
443        }
444        PktLineFrame::Delimiter => {
445            packet_trace(b"0001", true);
446            writer.write_all(b"0001")?;
447        }
448        PktLineFrame::ResponseEnd => {
449            packet_trace(b"0002", true);
450            writer.write_all(b"0002")?;
451        }
452    }
453    Ok(())
454}
455
456pub fn write_pkt_line_payload(writer: &mut impl Write, payload: &[u8]) -> Result<()> {
457    validate_pkt_line_payload(payload)?;
458    packet_trace(payload, true);
459    let len = payload.len() + 4;
460    writer.write_all(&pkt_line_header(len))?;
461    writer.write_all(payload)?;
462    Ok(())
463}
464
465pub fn write_pkt_line_frames(writer: &mut impl Write, frames: &[PktLineFrame]) -> Result<()> {
466    for frame in frames {
467        write_pkt_line_frame(writer, frame)?;
468    }
469    Ok(())
470}
471
472pub fn parse_error_line(payload: &[u8]) -> Result<ProtocolErrorLine> {
473    let text = parse_protocol_v2_line_text("protocol error line", payload)?;
474    let Some(message) = text.strip_prefix("ERR ") else {
475        return Err(GitError::InvalidFormat(
476            "protocol error line must start with ERR".into(),
477        ));
478    };
479    validate_protocol_error_message(message)?;
480    Ok(ProtocolErrorLine {
481        message: message.to_string(),
482    })
483}
484
485pub fn encode_error_line(error: &ProtocolErrorLine) -> Result<Vec<u8>> {
486    validate_protocol_error_message(&error.message)?;
487    Ok(line_from_str(&format!("ERR {}", error.message)))
488}
489
490pub fn parse_error_frame(frame: &PktLineFrame) -> Result<Option<ProtocolErrorLine>> {
491    match frame {
492        PktLineFrame::Data(payload) if trim_trailing_lf(payload).starts_with(b"ERR ") => {
493            parse_error_line(payload).map(Some)
494        }
495        PktLineFrame::Data(_)
496        | PktLineFrame::Flush
497        | PktLineFrame::Delimiter
498        | PktLineFrame::ResponseEnd => Ok(None),
499    }
500}
501
502pub fn read_error_line(reader: &mut impl Read) -> Result<ProtocolErrorLine> {
503    let Some(frame) = read_pkt_line_frame(reader)? else {
504        return Err(GitError::InvalidFormat(
505            "pkt-line stream ended before protocol error line".into(),
506        ));
507    };
508    match frame {
509        PktLineFrame::Data(payload) => parse_error_line(&payload),
510        _ => Err(GitError::InvalidFormat(
511            "protocol error line must be a data packet".into(),
512        )),
513    }
514}
515
516pub fn write_error_line(writer: &mut impl Write, error: &ProtocolErrorLine) -> Result<()> {
517    write_pkt_line_frame(writer, &PktLineFrame::data(encode_error_line(error)?)?)
518}
519
520pub fn parse_git_service(value: &str) -> Result<GitService> {
521    match value {
522        "git-upload-pack" => Ok(GitService::UploadPack),
523        "git-receive-pack" => Ok(GitService::ReceivePack),
524        "git-upload-archive" => Ok(GitService::UploadArchive),
525        other => Err(GitError::InvalidFormat(format!(
526            "unsupported git service {other}"
527        ))),
528    }
529}
530
531pub fn parse_refspec(value: &str) -> Result<RefSpec> {
532    validate_refspec_value(value)?;
533    let (force, value) = value
534        .strip_prefix('+')
535        .map_or((false, value), |value| (true, value));
536    let (negative, value) = value
537        .strip_prefix('^')
538        .map_or((false, value), |value| (true, value));
539    if force && negative {
540        return Err(GitError::InvalidFormat(
541            "negative refspec must not be forced".into(),
542        ));
543    }
544    let (src, dst) = if negative {
545        if value.contains(':') {
546            return Err(GitError::InvalidFormat(
547                "negative refspec must not have a destination".into(),
548            ));
549        }
550        (Some(value), None)
551    } else if let Some((src, dst)) = value.split_once(':') {
552        (non_empty(src), non_empty(dst))
553    } else {
554        (Some(value), None)
555    };
556    if src.is_none() && dst.is_none() && value != ":" {
557        return Err(GitError::InvalidFormat(
558            "refspec must include a source or destination".into(),
559        ));
560    }
561    if negative && src.is_none() {
562        return Err(GitError::InvalidFormat(
563            "negative refspec is missing a source".into(),
564        ));
565    }
566    if let Some(src) = src {
567        validate_refspec_endpoint("refspec source", src)?;
568    }
569    if let Some(dst) = dst {
570        validate_refspec_endpoint("refspec destination", dst)?;
571    }
572    let src_pattern_count = src.map(count_refspec_wildcards).unwrap_or(0);
573    let dst_pattern_count = dst.map(count_refspec_wildcards).unwrap_or(0);
574    if src_pattern_count > 1 || dst_pattern_count > 1 {
575        return Err(GitError::InvalidFormat(
576            "refspec endpoint has too many wildcards".into(),
577        ));
578    }
579    if dst.is_some() && (src_pattern_count == 1) != (dst_pattern_count == 1) {
580        return Err(GitError::InvalidFormat(
581            "refspec wildcard must appear in both source and destination".into(),
582        ));
583    }
584    Ok(RefSpec {
585        force,
586        negative,
587        src: src.map(str::to_string),
588        dst: dst.map(str::to_string),
589        pattern: src_pattern_count == 1 || dst_pattern_count == 1,
590    })
591}
592
593pub fn encode_refspec(refspec: &RefSpec) -> Result<String> {
594    validate_refspec_shape(refspec)?;
595    let mut out = String::new();
596    if refspec.force {
597        out.push('+');
598    }
599    if refspec.negative {
600        out.push('^');
601    }
602    if let Some(src) = &refspec.src {
603        out.push_str(src);
604    }
605    if !refspec.negative && refspec.src.is_none() && refspec.dst.is_none() {
606        out.push(':');
607    } else if !refspec.negative && refspec.dst.is_some() {
608        out.push(':');
609        if let Some(dst) = &refspec.dst {
610            out.push_str(dst);
611        }
612    }
613    Ok(out)
614}
615
616pub fn refspec_matches_source(refspec: &RefSpec, source: &str) -> Result<bool> {
617    Ok(refspec_map_source(refspec, source)?.is_some())
618}
619
620pub fn refspec_map_source(refspec: &RefSpec, source: &str) -> Result<Option<String>> {
621    validate_refspec_shape(refspec)?;
622    validate_refspec_endpoint("refspec match source", source)?;
623    let Some(src) = refspec.src.as_deref() else {
624        return Ok(None);
625    };
626    if refspec.pattern {
627        let Some((src_prefix, src_suffix)) = src.split_once('*') else {
628            return Ok(None);
629        };
630        let Some(middle) = source
631            .strip_prefix(src_prefix)
632            .and_then(|value| value.strip_suffix(src_suffix))
633        else {
634            return Ok(None);
635        };
636        if let Some(dst) = refspec.dst.as_deref() {
637            let (dst_prefix, dst_suffix) = dst.split_once('*').ok_or_else(|| {
638                GitError::InvalidFormat("pattern refspec destination is missing wildcard".into())
639            })?;
640            return Ok(Some(format!("{dst_prefix}{middle}{dst_suffix}")));
641        }
642        return Ok(Some(source.to_string()));
643    }
644    if src == source {
645        return Ok(Some(
646            refspec.dst.clone().unwrap_or_else(|| source.to_string()),
647        ));
648    }
649    Ok(None)
650}
651
652pub fn fetch_head_ref_description(refname: &str) -> Result<String> {
653    validate_fetch_head_description_field(refname)?;
654    // Mirror git's `kind`/`what` split in builtin/fetch.c: `HEAD` yields an empty
655    // note (no `'…' of` prefix at all), the standard ref namespaces get their
656    // kind word, and any other ref name is quoted bare.
657    if refname == "HEAD" {
658        Ok(String::new())
659    } else if let Some(branch) = refname.strip_prefix("refs/heads/") {
660        Ok(format!("branch '{branch}'"))
661    } else if let Some(tag) = refname.strip_prefix("refs/tags/") {
662        Ok(format!("tag '{tag}'"))
663    } else if let Some(rest) = refname.strip_prefix("refs/remotes/") {
664        Ok(format!("remote-tracking branch '{rest}'"))
665    } else {
666        Ok(format!("'{refname}'"))
667    }
668}
669
670pub fn fetch_head_remote_description(refname: &str, remote: &str) -> Result<String> {
671    validate_fetch_head_description_field(remote)?;
672    // git only appends `of <url>` when the note (`what`) is non-empty; a bare
673    // `HEAD` fetch records just the URL with an empty description.
674    let what = fetch_head_ref_description(refname)?;
675    if what.is_empty() {
676        Ok(remote.to_string())
677    } else {
678        Ok(format!("{what} of {remote}"))
679    }
680}
681
682pub fn parse_fetch_head(format: ObjectFormat, input: &[u8]) -> Result<Vec<FetchHeadRecord>> {
683    if input.is_empty() {
684        return Ok(Vec::new());
685    }
686    input
687        .split_inclusive(|byte| *byte == b'\n')
688        .map(|line| parse_fetch_head_record(format, line))
689        .collect()
690}
691
692pub fn encode_fetch_head(records: &[FetchHeadRecord]) -> Result<Vec<u8>> {
693    let mut out = Vec::new();
694    for record in records {
695        validate_fetch_head_description_field(&record.description)?;
696        out.extend_from_slice(record.oid.to_string().as_bytes());
697        out.push(b'\t');
698        if record.not_for_merge {
699            out.extend_from_slice(b"not-for-merge");
700        }
701        out.push(b'\t');
702        out.extend_from_slice(record.description.as_bytes());
703        out.push(b'\n');
704    }
705    Ok(out)
706}
707
708pub fn read_fetch_head(
709    format: ObjectFormat,
710    reader: &mut impl Read,
711) -> Result<Vec<FetchHeadRecord>> {
712    let mut input = Vec::new();
713    reader.read_to_end(&mut input)?;
714    parse_fetch_head(format, &input)
715}
716
717pub fn write_fetch_head(writer: &mut impl Write, records: &[FetchHeadRecord]) -> Result<()> {
718    for record in records {
719        validate_fetch_head_description_field(&record.description)?;
720        writer.write_all(record.oid.to_string().as_bytes())?;
721        writer.write_all(b"\t")?;
722        if record.not_for_merge {
723            writer.write_all(b"not-for-merge")?;
724        }
725        writer.write_all(b"\t")?;
726        writer.write_all(record.description.as_bytes())?;
727        writer.write_all(b"\n")?;
728    }
729    Ok(())
730}
731
732/// Match an abbreviated refspec source against the advertised refs the way
733/// upstream's `find_ref_by_name_abbrev` (remote.c) does: score each
734/// advertisement with `refname_match`'s `ref_rev_parse_rules` (exact name
735/// first, then `refs/<name>`, `refs/tags/<name>`, `refs/heads/<name>`,
736/// `refs/remotes/<name>`, `refs/remotes/<name>/HEAD`) and keep the best.
737fn find_advertised_ref_by_name_abbrev<'a>(
738    refs: &'a [RefAdvertisement],
739    name: &str,
740) -> Option<&'a RefAdvertisement> {
741    let mut best: Option<(&RefAdvertisement, usize)> = None;
742    for reference in refs {
743        let score = fetch_refname_match_score(name, &reference.name);
744        if score > best.map(|(_, score)| score).unwrap_or(0) {
745            best = Some((reference, score));
746        }
747    }
748    best.map(|(reference, _)| reference)
749}
750
751/// `refname_match` (refs.c): non-zero when `abbrev` can mean `full`, with the
752/// magnitude giving disambiguation precedence (earlier rules win).
753fn fetch_refname_match_score(abbrev: &str, full: &str) -> usize {
754    let expansions = [
755        abbrev.to_string(),
756        format!("refs/{abbrev}"),
757        format!("refs/tags/{abbrev}"),
758        format!("refs/heads/{abbrev}"),
759        format!("refs/remotes/{abbrev}"),
760        format!("refs/remotes/{abbrev}/HEAD"),
761    ];
762    for (index, candidate) in expansions.iter().enumerate() {
763        if candidate == full {
764            return expansions.len() - index;
765        }
766    }
767    0
768}
769
770/// Whether `abbrev` (a possibly-abbreviated ref like `three` or `refs/heads/main`)
771/// matches the full ref `full` under git's `ref_rev_parse_rules` expansion, the
772/// way `refname_match`/`branch_merge_matches` (remote.c) compare a configured
773/// `branch.<name>.merge` value against an advertised ref name.
774pub fn refname_matches(abbrev: &str, full: &str) -> bool {
775    fetch_refname_match_score(abbrev, full) > 0
776}
777
778/// Qualify a fetch refspec destination the way upstream's `get_local_ref`
779/// (remote.c) does: `refs/...` stays as-is, `heads/`, `tags/` and `remotes/`
780/// gain a `refs/` prefix, and anything else lands under `refs/heads/`.
781fn fetch_local_ref_name(name: &str) -> String {
782    if name.starts_with("refs/") {
783        name.to_string()
784    } else if name.starts_with("heads/")
785        || name.starts_with("tags/")
786        || name.starts_with("remotes/")
787    {
788        format!("refs/{name}")
789    } else {
790        format!("refs/heads/{name}")
791    }
792}
793
794pub fn plan_fetch_ref_updates(
795    refs: &[RefAdvertisement],
796    refspecs: &[RefSpec],
797    auto_follow_tags: bool,
798) -> Result<Vec<FetchRefUpdate>> {
799    let negative = refspecs
800        .iter()
801        .filter(|refspec| refspec.negative)
802        .collect::<Vec<_>>();
803    let mut updates = Vec::new();
804    for refspec in refspecs.iter().filter(|refspec| !refspec.negative) {
805        validate_refspec_shape(refspec)?;
806        let Some(src) = refspec.src.as_deref() else {
807            return Err(GitError::InvalidFormat(
808                "fetch refspec is missing a source".into(),
809            ));
810        };
811        if refspec.pattern {
812            for reference in refs {
813                if refspec_is_excluded(&negative, &reference.name)? {
814                    continue;
815                }
816                if let Some(dst) = refspec_map_source(refspec, &reference.name)? {
817                    updates.push(FetchRefUpdate {
818                        src: reference.name.clone(),
819                        dst: Some(dst),
820                        oid: reference.oid,
821                        not_for_merge: false,
822                    });
823                }
824            }
825            continue;
826        }
827        if refspec_is_excluded(&negative, src)? {
828            continue;
829        }
830        let Some(reference) = find_advertised_ref_by_name_abbrev(refs, src) else {
831            return Err(GitError::reference_not_found(format!("remote ref {src}")));
832        };
833        updates.push(FetchRefUpdate {
834            src: reference.name.clone(),
835            dst: refspec.dst.as_deref().map(fetch_local_ref_name),
836            oid: reference.oid,
837            not_for_merge: false,
838        });
839    }
840    if auto_follow_tags && updates.iter().any(|update| update.dst.is_some()) {
841        let fetched_oids = updates.iter().map(|update| update.oid).collect::<Vec<_>>();
842        let fetched_srcs = updates
843            .iter()
844            .map(|update| update.src.clone())
845            .collect::<Vec<_>>();
846        for reference in refs {
847            if reference.name.starts_with("refs/tags/")
848                && fetched_oids.iter().any(|oid| oid == &reference.oid)
849                && !fetched_srcs.contains(&reference.name)
850                && !refspec_is_excluded(&negative, &reference.name)?
851            {
852                updates.push(FetchRefUpdate {
853                    src: reference.name.clone(),
854                    dst: Some(reference.name.clone()),
855                    oid: reference.oid,
856                    not_for_merge: true,
857                });
858            }
859        }
860    }
861    Ok(updates)
862}
863
864pub fn fetch_ref_updates_to_fetch_head(
865    updates: &[FetchRefUpdate],
866    remote: &str,
867) -> Result<Vec<FetchHeadRecord>> {
868    updates
869        .iter()
870        .map(|update| {
871            Ok(FetchHeadRecord {
872                oid: update.oid,
873                not_for_merge: update.not_for_merge,
874                description: fetch_head_remote_description(&update.src, remote)?,
875            })
876        })
877        .collect()
878}
879
880pub fn plan_push_commands(
881    format: ObjectFormat,
882    local_refs: &[PushSourceRef],
883    remote_refs: &[RefAdvertisement],
884    refspecs: &[RefSpec],
885) -> Result<Vec<ReceivePackCommand>> {
886    let zero = zero_object_id(format)?;
887    let mut commands = Vec::new();
888    for refspec in refspecs {
889        validate_refspec_shape(refspec)?;
890        if refspec.negative {
891            return Err(GitError::InvalidFormat(
892                "push refspec must not be negative".into(),
893            ));
894        }
895        match (refspec.src.as_deref(), refspec.dst.as_deref()) {
896            (None, None) => {
897                // A bare ":" (matching) refspec pushes only refs the remote
898                // already has, by their fully-qualified `refs/...` name. git's
899                // matching source set is the local ref advertisement, which never
900                // includes `HEAD` or short-name aliases — push those would try to
901                // update the remote's `HEAD`, so skip anything not under `refs/`.
902                for local in local_refs {
903                    if !local.name.starts_with("refs/") {
904                        continue;
905                    }
906                    validate_push_source_ref(format, local)?;
907                    if let Some(remote) = remote_ref(remote_refs, &local.name) {
908                        commands.push(ReceivePackCommand {
909                            old_id: remote.oid,
910                            new_id: local.oid,
911                            name: local.name.clone(),
912                        });
913                    }
914                }
915            }
916            (None, Some(dst)) => {
917                validate_refspec_endpoint("push destination", dst)?;
918                let remote = remote_ref(remote_refs, dst)
919                    .ok_or_else(|| GitError::reference_not_found(format!("remote ref {dst}")))?;
920                commands.push(ReceivePackCommand {
921                    old_id: remote.oid,
922                    new_id: zero.clone(),
923                    name: dst.to_string(),
924                });
925            }
926            (Some(src), dst) if refspec.pattern => {
927                let Some((src_prefix, src_suffix)) = src.split_once('*') else {
928                    return Err(GitError::InvalidFormat(
929                        "pattern push refspec source is missing wildcard".into(),
930                    ));
931                };
932                let dst = dst.ok_or_else(|| {
933                    GitError::InvalidFormat("pattern push refspec is missing destination".into())
934                })?;
935                let (dst_prefix, dst_suffix) = dst.split_once('*').ok_or_else(|| {
936                    GitError::InvalidFormat(
937                        "pattern push refspec destination is missing wildcard".into(),
938                    )
939                })?;
940                for local in local_refs {
941                    validate_push_source_ref(format, local)?;
942                    let Some(middle) = local
943                        .name
944                        .strip_prefix(src_prefix)
945                        .and_then(|value| value.strip_suffix(src_suffix))
946                    else {
947                        continue;
948                    };
949                    let name = format!("{dst_prefix}{middle}{dst_suffix}");
950                    let old_id = remote_ref(remote_refs, &name)
951                        .map(|reference| reference.oid)
952                        .unwrap_or_else(|| zero.clone());
953                    commands.push(ReceivePackCommand {
954                        old_id,
955                        new_id: local.oid,
956                        name,
957                    });
958                }
959            }
960            (Some(src), dst) => {
961                validate_refspec_endpoint("push source", src)?;
962                let local = local_ref(local_refs, src)
963                    .ok_or_else(|| GitError::reference_not_found(format!("local ref {src}")))?;
964                validate_push_source_ref(format, local)?;
965                let name = dst.unwrap_or(src);
966                validate_refspec_endpoint("push destination", name)?;
967                let old_id = remote_ref(remote_refs, name)
968                    .map(|reference| reference.oid)
969                    .unwrap_or_else(|| zero.clone());
970                commands.push(ReceivePackCommand {
971                    old_id,
972                    new_id: local.oid,
973                    name: name.to_string(),
974                });
975            }
976        }
977    }
978    Ok(commands)
979}
980
981pub fn build_receive_pack_push_request(
982    features: &ReceivePackFeatures,
983    commands: Vec<ReceivePackCommand>,
984    packfile: Vec<u8>,
985    options: ReceivePackPushRequestOptions,
986) -> Result<ReceivePackPushRequest> {
987    let mut capabilities = Vec::new();
988    if options.report_status_v2 {
989        require_receive_pack_feature(features.report_status_v2, "report-status-v2")?;
990        capabilities.push(Capability {
991            name: "report-status-v2".into(),
992            value: None,
993        });
994    } else if options.report_status {
995        require_receive_pack_feature(features.report_status, "report-status")?;
996        capabilities.push(Capability {
997            name: "report-status".into(),
998            value: None,
999        });
1000    }
1001    if commands.iter().any(is_receive_pack_delete_command) {
1002        require_receive_pack_feature(features.delete_refs, "delete-refs")?;
1003        capabilities.push(Capability {
1004            name: "delete-refs".into(),
1005            value: None,
1006        });
1007    }
1008    if options.atomic {
1009        require_receive_pack_feature(features.atomic, "atomic")?;
1010        capabilities.push(Capability {
1011            name: "atomic".into(),
1012            value: None,
1013        });
1014    }
1015    if options.ofs_delta {
1016        require_receive_pack_feature(features.ofs_delta, "ofs-delta")?;
1017        capabilities.push(Capability {
1018            name: "ofs-delta".into(),
1019            value: None,
1020        });
1021    }
1022    if options.side_band_64k {
1023        require_receive_pack_feature(features.side_band_64k, "side-band-64k")?;
1024        capabilities.push(Capability {
1025            name: "side-band-64k".into(),
1026            value: None,
1027        });
1028    }
1029    if options.quiet {
1030        require_receive_pack_feature(features.quiet, "quiet")?;
1031        capabilities.push(Capability {
1032            name: "quiet".into(),
1033            value: None,
1034        });
1035    }
1036    if let Some(agent) = &options.agent {
1037        validate_capability_field("receive-pack request agent", agent)?;
1038        capabilities.push(Capability {
1039            name: "agent".into(),
1040            value: Some(agent.clone()),
1041        });
1042    }
1043    if let Some(format) = options.object_format {
1044        if features.object_format != Some(format) {
1045            return Err(GitError::InvalidFormat(
1046                "receive-pack request object-format was not advertised".into(),
1047            ));
1048        }
1049        capabilities.push(Capability {
1050            name: "object-format".into(),
1051            value: Some(format.name().into()),
1052        });
1053    }
1054    let push_options = if options.push_options.is_empty() {
1055        None
1056    } else {
1057        require_receive_pack_feature(features.push_options, "push-options")?;
1058        for option in &options.push_options {
1059            validate_receive_pack_push_option(option.as_bytes())?;
1060        }
1061        capabilities.push(Capability {
1062            name: "push-options".into(),
1063            value: None,
1064        });
1065        Some(options.push_options)
1066    };
1067    let request = ReceivePackPushRequest {
1068        commands: ReceivePackRequest {
1069            commands,
1070            capabilities,
1071            shallow: Vec::new(),
1072        },
1073        push_options,
1074        packfile,
1075    };
1076    validate_receive_pack_push_request_features(features, &request)?;
1077    Ok(request)
1078}
1079
1080pub fn smart_http_info_refs_path(repository_path: &str, service: GitService) -> Result<String> {
1081    validate_smart_http_service(service)?;
1082    let repository_path = normalize_http_repository_path(repository_path)?;
1083    Ok(format!(
1084        "{repository_path}/info/refs?service={}",
1085        service.as_str()
1086    ))
1087}
1088
1089pub fn smart_http_rpc_path(repository_path: &str, service: GitService) -> Result<String> {
1090    validate_smart_http_service(service)?;
1091    let repository_path = normalize_http_repository_path(repository_path)?;
1092    Ok(format!("{repository_path}/{}", service.as_str()))
1093}
1094
1095pub fn dumb_http_info_refs_path(repository_path: &str) -> Result<String> {
1096    let repository_path = normalize_http_repository_path(repository_path)?;
1097    Ok(format!("{repository_path}/info/refs"))
1098}
1099
1100pub fn dumb_http_alternates_path(repository_path: &str) -> Result<String> {
1101    let repository_path = normalize_http_repository_path(repository_path)?;
1102    Ok(format!("{repository_path}/objects/info/http-alternates"))
1103}
1104
1105pub fn dumb_http_packs_path(repository_path: &str) -> Result<String> {
1106    let repository_path = normalize_http_repository_path(repository_path)?;
1107    Ok(format!("{repository_path}/objects/info/packs"))
1108}
1109
1110pub fn dumb_http_loose_object_path(repository_path: &str, oid: &ObjectId) -> Result<String> {
1111    let repository_path = normalize_http_repository_path(repository_path)?;
1112    let oid = oid.to_string();
1113    let (directory, file) = oid.split_at(2);
1114    Ok(format!("{repository_path}/objects/{directory}/{file}"))
1115}
1116
1117pub fn dumb_http_pack_file_path(repository_path: &str, hash: &ObjectId) -> Result<String> {
1118    dumb_http_pack_resource_path(repository_path, hash, "pack")
1119}
1120
1121pub fn dumb_http_pack_index_path(repository_path: &str, hash: &ObjectId) -> Result<String> {
1122    dumb_http_pack_resource_path(repository_path, hash, "idx")
1123}
1124
1125pub fn smart_http_advertisement_content_type(service: GitService) -> Result<String> {
1126    validate_smart_http_service(service)?;
1127    Ok(format!("application/x-{}-advertisement", service.as_str()))
1128}
1129
1130pub fn smart_http_rpc_request_content_type(service: GitService) -> Result<String> {
1131    validate_smart_http_service(service)?;
1132    Ok(format!("application/x-{}-request", service.as_str()))
1133}
1134
1135pub fn smart_http_rpc_result_content_type(service: GitService) -> Result<String> {
1136    validate_smart_http_service(service)?;
1137    Ok(format!("application/x-{}-result", service.as_str()))
1138}
1139
1140pub fn parse_smart_http_advertisement_content_type(value: &str) -> Result<GitService> {
1141    parse_smart_http_content_type(value, "-advertisement")
1142}
1143
1144pub fn parse_smart_http_rpc_request_content_type(value: &str) -> Result<GitService> {
1145    parse_smart_http_content_type(value, "-request")
1146}
1147
1148pub fn parse_smart_http_rpc_result_content_type(value: &str) -> Result<GitService> {
1149    parse_smart_http_content_type(value, "-result")
1150}
1151
1152pub fn parse_sideband_packet(payload: &[u8]) -> Result<SideBandPacket> {
1153    let Some((&channel, data)) = payload.split_first() else {
1154        return Err(GitError::InvalidFormat("sideband packet is empty".into()));
1155    };
1156    let channel = match channel {
1157        1 => SideBandChannel::Data,
1158        2 => SideBandChannel::Progress,
1159        3 => SideBandChannel::Fatal,
1160        other => {
1161            return Err(GitError::InvalidFormat(format!(
1162                "invalid sideband channel {other}"
1163            )));
1164        }
1165    };
1166    Ok(SideBandPacket {
1167        channel,
1168        data: data.to_vec(),
1169    })
1170}
1171
1172pub fn encode_sideband_packet(packet: &SideBandPacket) -> Result<Vec<u8>> {
1173    let mut out = Vec::with_capacity(packet.data.len() + 1);
1174    out.push(match packet.channel {
1175        SideBandChannel::Data => 1,
1176        SideBandChannel::Progress => 2,
1177        SideBandChannel::Fatal => 3,
1178    });
1179    out.extend_from_slice(&packet.data);
1180    if out.len() > PKT_LINE_MAX_PAYLOAD_LEN {
1181        return Err(GitError::InvalidFormat(format!(
1182            "sideband packet exceeds {PKT_LINE_MAX_PAYLOAD_LEN} bytes"
1183        )));
1184    }
1185    Ok(out)
1186}
1187
1188pub fn write_sideband_packet(writer: &mut impl Write, packet: &SideBandPacket) -> Result<()> {
1189    write_sideband_payload(writer, packet.channel, &packet.data)
1190}
1191
1192fn write_sideband_payload(
1193    writer: &mut impl Write,
1194    channel: SideBandChannel,
1195    data: &[u8],
1196) -> Result<()> {
1197    let payload_len = data
1198        .len()
1199        .checked_add(1)
1200        .ok_or_else(|| GitError::InvalidFormat("sideband packet length overflow".into()))?;
1201    if payload_len > PKT_LINE_MAX_PAYLOAD_LEN {
1202        return Err(GitError::InvalidFormat(format!(
1203            "sideband packet exceeds {PKT_LINE_MAX_PAYLOAD_LEN} bytes"
1204        )));
1205    }
1206    writer.write_all(&pkt_line_header(payload_len + 4))?;
1207    writer.write_all(&[match channel {
1208        SideBandChannel::Data => 1,
1209        SideBandChannel::Progress => 2,
1210        SideBandChannel::Fatal => 3,
1211    }])?;
1212    writer.write_all(data)?;
1213    Ok(())
1214}
1215
1216pub fn parse_sideband_packets(payloads: &[Vec<u8>]) -> Result<Vec<SideBandPacket>> {
1217    payloads
1218        .iter()
1219        .map(|payload| parse_sideband_packet(payload))
1220        .collect()
1221}
1222
1223pub fn encode_sideband_packets(packets: &[SideBandPacket]) -> Result<Vec<Vec<u8>>> {
1224    packets.iter().map(encode_sideband_packet).collect()
1225}
1226
1227pub fn parse_sideband_stream(frames: &[PktLineFrame]) -> Result<Vec<SideBandPacket>> {
1228    let mut packets = Vec::new();
1229    let mut saw_flush = false;
1230    for (idx, frame) in frames.iter().enumerate() {
1231        match frame {
1232            PktLineFrame::Data(payload) if !saw_flush => {
1233                packets.push(parse_sideband_packet(payload)?);
1234            }
1235            PktLineFrame::Data(_) => {
1236                return Err(GitError::InvalidFormat(
1237                    "sideband stream has data after flush".into(),
1238                ));
1239            }
1240            PktLineFrame::Flush => {
1241                saw_flush = true;
1242                if idx + 1 != frames.len() {
1243                    return Err(GitError::InvalidFormat(
1244                        "sideband stream has frames after flush".into(),
1245                    ));
1246                }
1247            }
1248            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
1249                return Err(GitError::InvalidFormat(
1250                    "sideband stream contains a non-flush control packet".into(),
1251                ));
1252            }
1253        }
1254    }
1255    if !saw_flush {
1256        return Err(GitError::InvalidFormat(
1257            "sideband stream missing flush".into(),
1258        ));
1259    }
1260    Ok(packets)
1261}
1262
1263pub fn encode_sideband_stream(packets: &[SideBandPacket]) -> Result<Vec<PktLineFrame>> {
1264    let mut frames = Vec::new();
1265    for packet in packets {
1266        frames.push(PktLineFrame::data(encode_sideband_packet(packet)?)?);
1267    }
1268    frames.push(PktLineFrame::Flush);
1269    Ok(frames)
1270}
1271
1272pub fn read_sideband_stream(reader: &mut impl Read) -> Result<Vec<SideBandPacket>> {
1273    let frames = read_pkt_line_frames_until_flush(reader)?;
1274    parse_sideband_stream(&frames)
1275}
1276
1277pub fn write_sideband_stream(writer: &mut impl Write, packets: &[SideBandPacket]) -> Result<()> {
1278    for packet in packets {
1279        write_sideband_packet(writer, packet)?;
1280    }
1281    writer.write_all(b"0000")?;
1282    Ok(())
1283}
1284
1285pub fn demux_sideband_packets(packets: &[SideBandPacket]) -> Result<SideBandDemux> {
1286    let mut out = SideBandDemux::default();
1287    for packet in packets {
1288        match packet.channel {
1289            SideBandChannel::Data => out.data.extend_from_slice(&packet.data),
1290            SideBandChannel::Progress => out.progress.push(packet.data.clone()),
1291            SideBandChannel::Fatal => {
1292                let message = String::from_utf8_lossy(&packet.data).into_owned();
1293                return Err(GitError::InvalidFormat(format!(
1294                    "sideband fatal: {message}"
1295                )));
1296            }
1297        }
1298    }
1299    Ok(out)
1300}
1301
1302pub fn parse_and_demux_sideband_packets(payloads: &[Vec<u8>]) -> Result<SideBandDemux> {
1303    let packets = parse_sideband_packets(payloads)?;
1304    demux_sideband_packets(&packets)
1305}
1306
1307pub fn demux_sideband_stream(frames: &[PktLineFrame]) -> Result<SideBandDemux> {
1308    let packets = parse_sideband_stream(frames)?;
1309    demux_sideband_packets(&packets)
1310}
1311
1312pub fn read_and_demux_sideband_stream(reader: &mut impl Read) -> Result<SideBandDemux> {
1313    let packets = read_sideband_stream(reader)?;
1314    demux_sideband_packets(&packets)
1315}
1316
1317pub fn parse_upload_archive_request(frames: &[PktLineFrame]) -> Result<UploadArchiveRequest> {
1318    let mut request = UploadArchiveRequest::default();
1319    let mut saw_flush = false;
1320    for (idx, frame) in frames.iter().enumerate() {
1321        match frame {
1322            PktLineFrame::Data(payload) if !saw_flush => {
1323                let text = parse_protocol_v2_line_text("upload-archive request argument", payload)?;
1324                let argument = text.strip_prefix("argument ").ok_or_else(|| {
1325                    GitError::InvalidFormat("upload-archive request line must be argument".into())
1326                })?;
1327                validate_upload_archive_argument(argument)?;
1328                request.arguments.push(argument.to_string());
1329            }
1330            PktLineFrame::Data(_) => {
1331                return Err(GitError::InvalidFormat(
1332                    "upload-archive request has data after flush".into(),
1333                ));
1334            }
1335            PktLineFrame::Flush => {
1336                saw_flush = true;
1337                if idx + 1 != frames.len() {
1338                    return Err(GitError::InvalidFormat(
1339                        "upload-archive request has frames after flush".into(),
1340                    ));
1341                }
1342            }
1343            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
1344                return Err(GitError::InvalidFormat(
1345                    "upload-archive request contains a non-flush control packet".into(),
1346                ));
1347            }
1348        }
1349    }
1350    if !saw_flush {
1351        return Err(GitError::InvalidFormat(
1352            "upload-archive request missing flush".into(),
1353        ));
1354    }
1355    if request.arguments.is_empty() {
1356        return Err(GitError::InvalidFormat(
1357            "upload-archive request is missing arguments".into(),
1358        ));
1359    }
1360    Ok(request)
1361}
1362
1363pub fn encode_upload_archive_request(request: &UploadArchiveRequest) -> Result<Vec<PktLineFrame>> {
1364    if request.arguments.is_empty() {
1365        return Err(GitError::InvalidFormat(
1366            "upload-archive request is missing arguments".into(),
1367        ));
1368    }
1369    let mut frames = Vec::new();
1370    for argument in &request.arguments {
1371        validate_upload_archive_argument(argument)?;
1372        frames.push(PktLineFrame::data(line_from_str(&format!(
1373            "argument {argument}"
1374        )))?);
1375    }
1376    frames.push(PktLineFrame::Flush);
1377    Ok(frames)
1378}
1379
1380pub fn read_upload_archive_request(reader: &mut impl Read) -> Result<UploadArchiveRequest> {
1381    let frames = read_pkt_line_frames_until_flush(reader)?;
1382    parse_upload_archive_request(&frames)
1383}
1384
1385pub fn write_upload_archive_request(
1386    writer: &mut impl Write,
1387    request: &UploadArchiveRequest,
1388) -> Result<()> {
1389    if request.arguments.is_empty() {
1390        return Err(GitError::InvalidFormat(
1391            "upload-archive request is missing arguments".into(),
1392        ));
1393    }
1394    for argument in &request.arguments {
1395        validate_upload_archive_argument(argument)?;
1396        write_pkt_line_payload(writer, &line_from_str(&format!("argument {argument}")))?;
1397    }
1398    writer.write_all(b"0000")?;
1399    Ok(())
1400}
1401
1402pub fn parse_upload_archive_response(frames: &[PktLineFrame]) -> Result<UploadArchiveResponse> {
1403    let Some((first, rest)) = frames.split_first() else {
1404        return Err(GitError::InvalidFormat(
1405            "upload-archive response is empty".into(),
1406        ));
1407    };
1408    let PktLineFrame::Data(payload) = first else {
1409        return Err(GitError::InvalidFormat(
1410            "upload-archive response must start with a data packet".into(),
1411        ));
1412    };
1413    let text = parse_protocol_v2_line_text("upload-archive response status", payload)?;
1414    if text == "ACK" {
1415        return Ok(UploadArchiveResponse::Ack {
1416            sideband: parse_sideband_stream(rest)?,
1417        });
1418    }
1419    if let Some(message) = text.strip_prefix("NACK ") {
1420        validate_upload_archive_status_message(message)?;
1421        if !matches!(rest, [PktLineFrame::Flush]) {
1422            return Err(GitError::InvalidFormat(
1423                "upload-archive NACK response must end with flush".into(),
1424            ));
1425        }
1426        return Ok(UploadArchiveResponse::Nack {
1427            message: message.to_string(),
1428        });
1429    }
1430    Err(GitError::InvalidFormat(format!(
1431        "unsupported upload-archive response status {text}"
1432    )))
1433}
1434
1435pub fn encode_upload_archive_response(
1436    response: &UploadArchiveResponse,
1437) -> Result<Vec<PktLineFrame>> {
1438    let mut frames = Vec::new();
1439    match response {
1440        UploadArchiveResponse::Ack { sideband } => {
1441            frames.push(PktLineFrame::data(line_from_str("ACK"))?);
1442            frames.extend(encode_sideband_stream(sideband)?);
1443        }
1444        UploadArchiveResponse::Nack { message } => {
1445            validate_upload_archive_status_message(message)?;
1446            frames.push(PktLineFrame::data(line_from_str(&format!(
1447                "NACK {message}"
1448            )))?);
1449            frames.push(PktLineFrame::Flush);
1450        }
1451    }
1452    Ok(frames)
1453}
1454
1455pub fn read_upload_archive_response(reader: &mut impl Read) -> Result<UploadArchiveResponse> {
1456    let frames = read_pkt_line_frames_until_flush(reader)?;
1457    parse_upload_archive_response(&frames)
1458}
1459
1460pub fn write_upload_archive_response(
1461    writer: &mut impl Write,
1462    response: &UploadArchiveResponse,
1463) -> Result<()> {
1464    match response {
1465        UploadArchiveResponse::Ack { sideband } => {
1466            write_pkt_line_payload(writer, b"ACK\n")?;
1467            write_sideband_stream(writer, sideband)?;
1468        }
1469        UploadArchiveResponse::Nack { message } => {
1470            validate_upload_archive_status_message(message)?;
1471            write_pkt_line_payload(writer, &line_from_str(&format!("NACK {message}")))?;
1472            writer.write_all(b"0000")?;
1473        }
1474    }
1475    Ok(())
1476}
1477
1478pub fn demux_upload_archive_response(response: &UploadArchiveResponse) -> Result<SideBandDemux> {
1479    match response {
1480        UploadArchiveResponse::Ack { sideband } => demux_sideband_packets(sideband),
1481        UploadArchiveResponse::Nack { message } => Err(GitError::InvalidFormat(format!(
1482            "upload-archive NACK: {message}"
1483        ))),
1484    }
1485}
1486
1487fn parse_pkt_len(bytes: &[u8]) -> Result<usize> {
1488    let mut len = 0usize;
1489    for byte in bytes {
1490        len = (len << 4) | hex_nibble(*byte)? as usize;
1491    }
1492    Ok(len)
1493}
1494
1495fn hex_nibble(byte: u8) -> Result<u8> {
1496    match byte {
1497        b'0'..=b'9' => Ok(byte - b'0'),
1498        b'a'..=b'f' => Ok(byte - b'a' + 10),
1499        b'A'..=b'F' => Ok(byte - b'A' + 10),
1500        _ => Err(GitError::InvalidFormat(format!(
1501            "invalid pkt-line length byte {byte:#04x}"
1502        ))),
1503    }
1504}
1505
1506#[derive(Debug, Clone, PartialEq, Eq)]
1507pub struct TransportHandshake {
1508    pub protocol: ProtocolVersion,
1509    pub capabilities: Vec<Capability>,
1510}
1511
1512#[derive(Debug, Clone, PartialEq, Eq)]
1513pub struct RefAdvertisement {
1514    pub oid: ObjectId,
1515    pub name: String,
1516    pub capabilities: Vec<Capability>,
1517}
1518
1519#[derive(Debug, Clone, PartialEq, Eq)]
1520pub struct DumbHttpRefRecord {
1521    pub oid: ObjectId,
1522    pub name: String,
1523    pub peeled: bool,
1524}
1525
1526#[derive(Debug, Clone, PartialEq, Eq)]
1527pub struct DumbHttpPackRecord {
1528    pub hash: ObjectId,
1529}
1530
1531#[derive(Debug, Clone, PartialEq, Eq)]
1532pub struct RefAdvertisementSet {
1533    pub protocol: ProtocolVersion,
1534    pub refs: Vec<RefAdvertisement>,
1535    pub shallow: Vec<ObjectId>,
1536}
1537
1538#[derive(Debug, Clone, PartialEq, Eq, Default)]
1539pub struct UploadPackRequest {
1540    pub wants: Vec<ObjectId>,
1541    pub capabilities: Vec<Capability>,
1542    pub shallow: Vec<ObjectId>,
1543    pub deepen: Option<u32>,
1544    pub deepen_since: Option<u64>,
1545    pub deepen_not: Vec<String>,
1546    pub filter: Option<String>,
1547}
1548
1549#[derive(Debug, Clone, PartialEq, Eq, Default)]
1550pub struct UploadPackFeatures {
1551    pub multi_ack: bool,
1552    pub multi_ack_detailed: bool,
1553    pub no_done: bool,
1554    pub thin_pack: bool,
1555    pub side_band: bool,
1556    pub side_band_64k: bool,
1557    pub ofs_delta: bool,
1558    pub shallow: bool,
1559    pub deepen_since: bool,
1560    pub deepen_not: bool,
1561    pub include_tag: bool,
1562    pub no_progress: bool,
1563    pub allow_tip_sha1_in_want: bool,
1564    pub allow_reachable_sha1_in_want: bool,
1565    pub filter: bool,
1566    pub agent: Option<String>,
1567    pub object_format: Option<ObjectFormat>,
1568    pub symrefs: Vec<String>,
1569    pub unknown: Vec<Capability>,
1570}
1571
1572#[derive(Debug, Clone, PartialEq, Eq, Default)]
1573pub struct UploadPackNegotiationRequest {
1574    pub haves: Vec<ObjectId>,
1575    pub done: bool,
1576}
1577
1578#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1579pub enum UploadPackAckStatus {
1580    Continue,
1581    Common,
1582    Ready,
1583}
1584
1585#[derive(Debug, Clone, PartialEq, Eq)]
1586pub enum UploadPackAcknowledgment {
1587    Nak,
1588    Ack {
1589        oid: ObjectId,
1590        status: Option<UploadPackAckStatus>,
1591    },
1592}
1593
1594#[derive(Debug, Clone, PartialEq, Eq, Default)]
1595pub struct UploadPackPackfileResponse {
1596    pub acknowledgments: Vec<UploadPackAcknowledgment>,
1597    pub sideband: Vec<SideBandPacket>,
1598}
1599
1600#[derive(Debug, Clone, PartialEq, Eq, Default)]
1601pub struct UploadPackRawPackfileResponse {
1602    pub acknowledgments: Vec<UploadPackAcknowledgment>,
1603    pub packfile: Vec<u8>,
1604}
1605
1606#[derive(Debug, Clone, PartialEq, Eq)]
1607pub struct ReceivePackCommand {
1608    pub old_id: ObjectId,
1609    pub new_id: ObjectId,
1610    pub name: String,
1611}
1612
1613#[derive(Debug, Clone, PartialEq, Eq, Default)]
1614pub struct ReceivePackRequest {
1615    pub shallow: Vec<ObjectId>,
1616    pub commands: Vec<ReceivePackCommand>,
1617    pub capabilities: Vec<Capability>,
1618}
1619
1620#[derive(Debug, Clone, PartialEq, Eq, Default)]
1621pub struct ReceivePackPushRequest {
1622    pub commands: ReceivePackRequest,
1623    pub push_options: Option<Vec<String>>,
1624    pub packfile: Vec<u8>,
1625}
1626
1627#[derive(Debug, Clone, PartialEq, Eq, Default)]
1628pub struct ReceivePackPushRequestOptions {
1629    pub report_status: bool,
1630    pub report_status_v2: bool,
1631    pub atomic: bool,
1632    pub ofs_delta: bool,
1633    pub side_band_64k: bool,
1634    pub quiet: bool,
1635    pub agent: Option<String>,
1636    pub object_format: Option<ObjectFormat>,
1637    pub push_options: Vec<String>,
1638}
1639
1640#[derive(Debug, Clone, PartialEq, Eq, Default)]
1641pub struct ReceivePackFeatures {
1642    pub report_status: bool,
1643    pub report_status_v2: bool,
1644    pub delete_refs: bool,
1645    pub ofs_delta: bool,
1646    pub atomic: bool,
1647    pub push_options: bool,
1648    pub side_band_64k: bool,
1649    pub quiet: bool,
1650    pub no_thin: bool,
1651    pub agent: Option<String>,
1652    pub object_format: Option<ObjectFormat>,
1653    pub unknown: Vec<Capability>,
1654}
1655
1656#[derive(Debug, Clone, PartialEq, Eq)]
1657pub enum ReceivePackUnpackStatus {
1658    Ok,
1659    Error(String),
1660}
1661
1662#[derive(Debug, Clone, PartialEq, Eq)]
1663pub enum ReceivePackCommandStatus {
1664    Ok { name: String },
1665    Ng { name: String, message: String },
1666}
1667
1668#[derive(Debug, Clone, PartialEq, Eq)]
1669pub struct ReceivePackReportStatus {
1670    pub unpack: ReceivePackUnpackStatus,
1671    pub commands: Vec<ReceivePackCommandStatus>,
1672}
1673
1674#[derive(Debug, Clone, PartialEq, Eq, Default)]
1675pub struct ReceivePackCommandStatusV2Options {
1676    pub refname: Option<String>,
1677    pub old_oid: Option<ObjectId>,
1678    pub new_oid: Option<ObjectId>,
1679    pub forced_update: bool,
1680}
1681
1682#[derive(Debug, Clone, PartialEq, Eq)]
1683pub enum ReceivePackCommandStatusV2 {
1684    Ok {
1685        name: String,
1686        options: ReceivePackCommandStatusV2Options,
1687    },
1688    Ng {
1689        name: String,
1690        message: String,
1691    },
1692}
1693
1694#[derive(Debug, Clone, PartialEq, Eq)]
1695pub struct ReceivePackReportStatusV2 {
1696    pub unpack: ReceivePackUnpackStatus,
1697    pub commands: Vec<ReceivePackCommandStatusV2>,
1698}
1699
1700#[derive(Debug, Clone, PartialEq, Eq)]
1701pub struct ProtocolV2CommandRequest {
1702    pub command: String,
1703    pub capabilities: Vec<Capability>,
1704    pub arguments: Vec<Vec<u8>>,
1705}
1706
1707#[derive(Debug, Clone, PartialEq, Eq)]
1708pub enum ProtocolV2Request {
1709    Command(ProtocolV2CommandRequest),
1710    Done,
1711}
1712
1713#[derive(Debug, Clone, PartialEq, Eq)]
1714pub enum ProtocolV2Command {
1715    LsRefs(ProtocolV2LsRefsRequest),
1716    Fetch(ProtocolV2FetchRequest),
1717    ObjectInfo(ProtocolV2ObjectInfoRequest),
1718    Unknown(ProtocolV2CommandRequest),
1719}
1720
1721#[derive(Debug, Clone, PartialEq, Eq)]
1722pub enum ProtocolV2SessionRequest {
1723    Command(ProtocolV2Command),
1724    Done,
1725}
1726
1727#[derive(Debug, Clone, PartialEq, Eq, Default)]
1728pub struct ProtocolV2CommandOptions {
1729    pub agent: Option<String>,
1730    pub object_format: Option<ObjectFormat>,
1731    pub server_options: Vec<String>,
1732    pub extra: Vec<Capability>,
1733}
1734
1735#[derive(Debug, Clone, PartialEq, Eq, Default)]
1736pub struct ProtocolV2FetchFeatures {
1737    pub shallow: bool,
1738    pub wait_for_done: bool,
1739    pub filter: bool,
1740    pub ref_in_want: bool,
1741    pub sideband_all: bool,
1742    pub packfile_uris: bool,
1743    pub unknown: Vec<String>,
1744}
1745
1746#[derive(Debug, Clone, PartialEq, Eq, Default)]
1747pub struct ProtocolV2LsRefsFeatures {
1748    pub unborn: bool,
1749    pub unknown: Vec<String>,
1750}
1751
1752impl ProtocolV2CommandRequest {
1753    pub fn new(command: impl Into<String>) -> Result<Self> {
1754        let command = command.into();
1755        validate_capability_name(&command)?;
1756        Ok(Self {
1757            command,
1758            capabilities: Vec::new(),
1759            arguments: Vec::new(),
1760        })
1761    }
1762}
1763
1764#[derive(Debug, Clone, PartialEq, Eq, Default)]
1765pub struct ProtocolV2LsRefsRequest {
1766    pub peel: bool,
1767    pub symrefs: bool,
1768    pub unborn: bool,
1769    pub ref_prefixes: Vec<String>,
1770}
1771
1772#[derive(Debug, Clone, PartialEq, Eq)]
1773pub struct ProtocolV2LsRefsRef {
1774    pub oid: ObjectId,
1775    pub name: String,
1776    pub peeled: Option<ObjectId>,
1777    pub symref_target: Option<String>,
1778    pub attributes: Vec<String>,
1779}
1780
1781#[derive(Debug, Clone, PartialEq, Eq)]
1782pub enum ProtocolV2LsRefsRecord {
1783    Ref(ProtocolV2LsRefsRef),
1784    Unborn {
1785        name: String,
1786        symref_target: Option<String>,
1787        attributes: Vec<String>,
1788    },
1789}
1790
1791#[derive(Debug, Clone, PartialEq, Eq, Default)]
1792pub struct ProtocolV2FetchRequest {
1793    pub wants: Vec<ObjectId>,
1794    pub want_refs: Vec<String>,
1795    pub haves: Vec<ObjectId>,
1796    pub shallow: Vec<ObjectId>,
1797    pub deepen: Option<u32>,
1798    pub deepen_since: Option<u64>,
1799    pub deepen_not: Vec<String>,
1800    pub deepen_relative: bool,
1801    pub filter: Option<String>,
1802    pub packfile_uris: Option<String>,
1803    pub thin_pack: bool,
1804    pub no_progress: bool,
1805    pub include_tag: bool,
1806    pub ofs_delta: bool,
1807    pub sideband_all: bool,
1808    pub wait_for_done: bool,
1809    pub done: bool,
1810}
1811
1812#[derive(Debug, Clone, PartialEq, Eq)]
1813pub enum ProtocolV2FetchAcknowledgment {
1814    Nak,
1815    Ack(ObjectId),
1816    Ready,
1817}
1818
1819#[derive(Debug, Clone, PartialEq, Eq)]
1820pub enum ProtocolV2FetchShallowInfo {
1821    Shallow(ObjectId),
1822    Unshallow(ObjectId),
1823}
1824
1825#[derive(Debug, Clone, PartialEq, Eq)]
1826pub struct ProtocolV2FetchWantedRef {
1827    pub oid: ObjectId,
1828    pub name: String,
1829}
1830
1831#[derive(Debug, Clone, PartialEq, Eq)]
1832pub struct ProtocolV2FetchPackfileUri {
1833    pub pack_hash: ObjectId,
1834    pub uri: String,
1835}
1836
1837#[derive(Debug, Clone, PartialEq, Eq)]
1838pub enum ProtocolV2FetchResponseSection {
1839    Acknowledgments(Vec<ProtocolV2FetchAcknowledgment>),
1840    ShallowInfo(Vec<ProtocolV2FetchShallowInfo>),
1841    WantedRefs(Vec<ProtocolV2FetchWantedRef>),
1842    PackfileUris(Vec<ProtocolV2FetchPackfileUri>),
1843    Packfile(Vec<Vec<u8>>),
1844    Unknown { name: String, lines: Vec<Vec<u8>> },
1845}
1846
1847#[derive(Debug, Clone, PartialEq, Eq, Default)]
1848pub struct ProtocolV2FetchSidebandAllResponse {
1849    pub sections: Vec<ProtocolV2FetchResponseSection>,
1850    pub progress: Vec<Vec<u8>>,
1851}
1852
1853#[derive(Debug, Clone, PartialEq, Eq, Default)]
1854pub struct ProtocolV2ObjectInfoRequest {
1855    pub size: bool,
1856    pub oids: Vec<ObjectId>,
1857}
1858
1859#[derive(Debug, Clone, PartialEq, Eq)]
1860pub struct ProtocolV2ObjectInfoRecord {
1861    pub oid: ObjectId,
1862    pub size: u64,
1863}
1864
1865#[derive(Debug, Clone, PartialEq, Eq, Default)]
1866pub struct ProtocolV2ObjectInfoResponse {
1867    pub size: bool,
1868    pub records: Vec<ProtocolV2ObjectInfoRecord>,
1869}
1870
1871impl ProtocolV2LsRefsRequest {
1872    pub fn from_command_request(request: &ProtocolV2CommandRequest) -> Result<Self> {
1873        if request.command != "ls-refs" {
1874            return Err(GitError::InvalidFormat(format!(
1875                "expected ls-refs command, got {}",
1876                request.command
1877            )));
1878        }
1879        let mut out = Self::default();
1880        for argument in &request.arguments {
1881            let text = std::str::from_utf8(argument)
1882                .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
1883            match text {
1884                "peel" => out.peel = true,
1885                "symrefs" => out.symrefs = true,
1886                "unborn" => out.unborn = true,
1887                value if value.starts_with("ref-prefix ") => {
1888                    let prefix = value
1889                        .strip_prefix("ref-prefix ")
1890                        .ok_or_else(|| GitError::InvalidFormat("invalid ref-prefix".into()))?;
1891                    validate_protocol_v2_token("ls-refs ref-prefix", prefix)?;
1892                    out.ref_prefixes.push(prefix.to_string());
1893                }
1894                other => {
1895                    return Err(GitError::InvalidFormat(format!(
1896                        "unsupported ls-refs argument {other}"
1897                    )));
1898                }
1899            }
1900        }
1901        Ok(out)
1902    }
1903
1904    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
1905        let mut request = ProtocolV2CommandRequest::new("ls-refs")?;
1906        if self.peel {
1907            request.arguments.push(b"peel".to_vec());
1908        }
1909        if self.symrefs {
1910            request.arguments.push(b"symrefs".to_vec());
1911        }
1912        if self.unborn {
1913            request.arguments.push(b"unborn".to_vec());
1914        }
1915        for prefix in &self.ref_prefixes {
1916            validate_protocol_v2_token("ls-refs ref-prefix", prefix)?;
1917            request
1918                .arguments
1919                .push(format!("ref-prefix {prefix}").into_bytes());
1920        }
1921        Ok(request)
1922    }
1923}
1924
1925impl ProtocolV2FetchRequest {
1926    pub fn from_command_request(
1927        format: ObjectFormat,
1928        request: &ProtocolV2CommandRequest,
1929    ) -> Result<Self> {
1930        if request.command != "fetch" {
1931            return Err(GitError::InvalidFormat(format!(
1932                "expected fetch command, got {}",
1933                request.command
1934            )));
1935        }
1936        let mut out = Self::default();
1937        for argument in &request.arguments {
1938            let text = std::str::from_utf8(argument)
1939                .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
1940            match text {
1941                "thin-pack" => out.thin_pack = true,
1942                "no-progress" => out.no_progress = true,
1943                "include-tag" => out.include_tag = true,
1944                "ofs-delta" => out.ofs_delta = true,
1945                "sideband-all" => out.sideband_all = true,
1946                "wait-for-done" => out.wait_for_done = true,
1947                "deepen-relative" => out.deepen_relative = true,
1948                "done" => out.done = true,
1949                value if value.starts_with("want ") => {
1950                    out.wants
1951                        .push(parse_oid_argument(format, "fetch want", value, "want ")?);
1952                }
1953                value if value.starts_with("want-ref ") => {
1954                    let name = value
1955                        .strip_prefix("want-ref ")
1956                        .ok_or_else(|| GitError::InvalidFormat("invalid fetch want-ref".into()))?;
1957                    validate_protocol_v2_token("fetch want-ref", name)?;
1958                    out.want_refs.push(name.to_string());
1959                }
1960                value if value.starts_with("have ") => {
1961                    out.haves
1962                        .push(parse_oid_argument(format, "fetch have", value, "have ")?);
1963                }
1964                value if value.starts_with("shallow ") => {
1965                    out.shallow.push(parse_oid_argument(
1966                        format,
1967                        "fetch shallow",
1968                        value,
1969                        "shallow ",
1970                    )?);
1971                }
1972                value if value.starts_with("deepen ") => {
1973                    if out.deepen.is_some() {
1974                        return Err(GitError::InvalidFormat(
1975                            "fetch request has duplicate deepen".into(),
1976                        ));
1977                    }
1978                    out.deepen = Some(parse_u32_argument("fetch deepen", value, "deepen ")?);
1979                }
1980                value if value.starts_with("deepen-since ") => {
1981                    if out.deepen_since.is_some() {
1982                        return Err(GitError::InvalidFormat(
1983                            "fetch request has duplicate deepen-since".into(),
1984                        ));
1985                    }
1986                    out.deepen_since = Some(parse_u64_argument(
1987                        "fetch deepen-since",
1988                        value,
1989                        "deepen-since ",
1990                    )?);
1991                }
1992                value if value.starts_with("deepen-not ") => {
1993                    let name = value.strip_prefix("deepen-not ").ok_or_else(|| {
1994                        GitError::InvalidFormat("invalid fetch deepen-not".into())
1995                    })?;
1996                    validate_protocol_v2_token("fetch deepen-not", name)?;
1997                    out.deepen_not.push(name.to_string());
1998                }
1999                value if value.starts_with("filter ") => {
2000                    if out.filter.is_some() {
2001                        return Err(GitError::InvalidFormat(
2002                            "fetch request has duplicate filter".into(),
2003                        ));
2004                    }
2005                    let filter = value
2006                        .strip_prefix("filter ")
2007                        .ok_or_else(|| GitError::InvalidFormat("invalid fetch filter".into()))?;
2008                    validate_protocol_v2_token("fetch filter", filter)?;
2009                    out.filter = Some(filter.to_string());
2010                }
2011                value if value.starts_with("packfile-uris ") => {
2012                    if out.packfile_uris.is_some() {
2013                        return Err(GitError::InvalidFormat(
2014                            "fetch request has duplicate packfile-uris".into(),
2015                        ));
2016                    }
2017                    let protocols = value.strip_prefix("packfile-uris ").ok_or_else(|| {
2018                        GitError::InvalidFormat("invalid fetch packfile-uris".into())
2019                    })?;
2020                    validate_protocol_v2_token("fetch packfile-uris", protocols)?;
2021                    out.packfile_uris = Some(protocols.to_string());
2022                }
2023                other => {
2024                    return Err(GitError::InvalidFormat(format!(
2025                        "unsupported fetch argument {other}"
2026                    )));
2027                }
2028            }
2029        }
2030        Ok(out)
2031    }
2032
2033    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
2034        let mut request = ProtocolV2CommandRequest::new("fetch")?;
2035        for oid in &self.wants {
2036            request.arguments.push(format!("want {oid}").into_bytes());
2037        }
2038        for name in &self.want_refs {
2039            validate_protocol_v2_token("fetch want-ref", name)?;
2040            request
2041                .arguments
2042                .push(format!("want-ref {name}").into_bytes());
2043        }
2044        for oid in &self.haves {
2045            request.arguments.push(format!("have {oid}").into_bytes());
2046        }
2047        for oid in &self.shallow {
2048            request
2049                .arguments
2050                .push(format!("shallow {oid}").into_bytes());
2051        }
2052        if let Some(deepen) = self.deepen {
2053            if deepen == 0 {
2054                return Err(GitError::InvalidFormat(
2055                    "fetch deepen must be positive".into(),
2056                ));
2057            }
2058            request
2059                .arguments
2060                .push(format!("deepen {deepen}").into_bytes());
2061        }
2062        if let Some(deepen_since) = self.deepen_since {
2063            request
2064                .arguments
2065                .push(format!("deepen-since {deepen_since}").into_bytes());
2066        }
2067        for name in &self.deepen_not {
2068            validate_protocol_v2_token("fetch deepen-not", name)?;
2069            request
2070                .arguments
2071                .push(format!("deepen-not {name}").into_bytes());
2072        }
2073        if self.deepen_relative {
2074            request.arguments.push(b"deepen-relative".to_vec());
2075        }
2076        if let Some(filter) = &self.filter {
2077            validate_protocol_v2_token("fetch filter", filter)?;
2078            request
2079                .arguments
2080                .push(format!("filter {filter}").into_bytes());
2081        }
2082        if let Some(protocols) = &self.packfile_uris {
2083            validate_protocol_v2_token("fetch packfile-uris", protocols)?;
2084            request
2085                .arguments
2086                .push(format!("packfile-uris {protocols}").into_bytes());
2087        }
2088        if self.thin_pack {
2089            request.arguments.push(b"thin-pack".to_vec());
2090        }
2091        if self.no_progress {
2092            request.arguments.push(b"no-progress".to_vec());
2093        }
2094        if self.include_tag {
2095            request.arguments.push(b"include-tag".to_vec());
2096        }
2097        if self.ofs_delta {
2098            request.arguments.push(b"ofs-delta".to_vec());
2099        }
2100        if self.sideband_all {
2101            request.arguments.push(b"sideband-all".to_vec());
2102        }
2103        if self.wait_for_done {
2104            request.arguments.push(b"wait-for-done".to_vec());
2105        }
2106        if self.done {
2107            request.arguments.push(b"done".to_vec());
2108        }
2109        Ok(request)
2110    }
2111}
2112
2113impl ProtocolV2ObjectInfoRequest {
2114    pub fn from_command_request(
2115        format: ObjectFormat,
2116        request: &ProtocolV2CommandRequest,
2117    ) -> Result<Self> {
2118        if request.command != "object-info" {
2119            return Err(GitError::InvalidFormat(format!(
2120                "expected object-info command, got {}",
2121                request.command
2122            )));
2123        }
2124        let mut out = Self::default();
2125        for argument in &request.arguments {
2126            let text = parse_protocol_v2_line_text("object-info request argument", argument)?;
2127            if text == "size" {
2128                if out.size {
2129                    return Err(GitError::InvalidFormat(
2130                        "object-info request has duplicate size argument".into(),
2131                    ));
2132                }
2133                out.size = true;
2134            } else if text.starts_with("oid ") {
2135                out.oids
2136                    .push(parse_oid_argument(format, "object-info oid", text, "oid ")?);
2137            } else {
2138                return Err(GitError::InvalidFormat(format!(
2139                    "unsupported object-info request argument {text}"
2140                )));
2141            }
2142        }
2143        if !out.size {
2144            return Err(GitError::InvalidFormat(
2145                "object-info request is missing size argument".into(),
2146            ));
2147        }
2148        if out.oids.is_empty() {
2149            return Err(GitError::InvalidFormat(
2150                "object-info request is missing object ids".into(),
2151            ));
2152        }
2153        Ok(out)
2154    }
2155
2156    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
2157        if !self.size {
2158            return Err(GitError::InvalidFormat(
2159                "object-info request is missing size argument".into(),
2160            ));
2161        }
2162        if self.oids.is_empty() {
2163            return Err(GitError::InvalidFormat(
2164                "object-info request is missing object ids".into(),
2165            ));
2166        }
2167        let mut request = ProtocolV2CommandRequest::new("object-info")?;
2168        request.arguments.push(b"size".to_vec());
2169        for oid in &self.oids {
2170            request.arguments.push(format!("oid {oid}").into_bytes());
2171        }
2172        Ok(request)
2173    }
2174}
2175
2176pub fn parse_protocol_v2_advertisement(frames: &[PktLineFrame]) -> Result<TransportHandshake> {
2177    let Some((first, rest)) = frames.split_first() else {
2178        return Err(GitError::InvalidFormat(
2179            "protocol v2 advertisement is empty".into(),
2180        ));
2181    };
2182    match first {
2183        PktLineFrame::Data(payload) if trim_trailing_lf(payload) == b"version 2" => {}
2184        PktLineFrame::Data(_) => {
2185            return Err(GitError::InvalidFormat(
2186                "protocol v2 advertisement missing version line".into(),
2187            ));
2188        }
2189        _ => {
2190            return Err(GitError::InvalidFormat(
2191                "protocol v2 advertisement must start with a data line".into(),
2192            ));
2193        }
2194    }
2195
2196    let mut capabilities = Vec::new();
2197    let mut saw_flush = false;
2198    for (idx, frame) in rest.iter().enumerate() {
2199        match frame {
2200            PktLineFrame::Data(payload) => {
2201                if saw_flush {
2202                    return Err(GitError::InvalidFormat(
2203                        "protocol v2 advertisement has data after flush".into(),
2204                    ));
2205                }
2206                capabilities.push(parse_protocol_v2_capability_line(payload)?);
2207            }
2208            PktLineFrame::Flush => {
2209                saw_flush = true;
2210                if idx + 1 != rest.len() {
2211                    return Err(GitError::InvalidFormat(
2212                        "protocol v2 advertisement has frames after flush".into(),
2213                    ));
2214                }
2215            }
2216            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
2217                return Err(GitError::InvalidFormat(
2218                    "protocol v2 advertisement contains a non-flush control packet".into(),
2219                ));
2220            }
2221        }
2222    }
2223    if !saw_flush {
2224        return Err(GitError::InvalidFormat(
2225            "protocol v2 advertisement missing flush".into(),
2226        ));
2227    }
2228
2229    Ok(TransportHandshake {
2230        protocol: ProtocolVersion::V2,
2231        capabilities,
2232    })
2233}
2234
2235pub fn encode_protocol_v2_advertisement(
2236    handshake: &TransportHandshake,
2237) -> Result<Vec<PktLineFrame>> {
2238    if handshake.protocol != ProtocolVersion::V2 {
2239        return Err(GitError::InvalidFormat(
2240            "protocol v2 advertisement requires a v2 handshake".into(),
2241        ));
2242    }
2243    let mut frames = vec![PktLineFrame::data(line_from_str("version 2"))?];
2244    for capability in &handshake.capabilities {
2245        frames.push(PktLineFrame::data(line(encode_protocol_v2_capability(
2246            capability,
2247        )?))?);
2248    }
2249    frames.push(PktLineFrame::Flush);
2250    Ok(frames)
2251}
2252
2253pub fn read_protocol_v2_advertisement(reader: &mut impl Read) -> Result<TransportHandshake> {
2254    let frames = read_pkt_line_frames_until_flush(reader)?;
2255    parse_protocol_v2_advertisement(&frames)
2256}
2257
2258pub fn write_protocol_v2_advertisement(
2259    writer: &mut impl Write,
2260    handshake: &TransportHandshake,
2261) -> Result<()> {
2262    if handshake.protocol != ProtocolVersion::V2 {
2263        return Err(GitError::InvalidFormat(
2264            "protocol v2 advertisement requires a v2 handshake".into(),
2265        ));
2266    }
2267    write_pkt_line_payload(writer, b"version 2\n")?;
2268    for capability in &handshake.capabilities {
2269        write_pkt_line_payload(writer, &line(encode_protocol_v2_capability(capability)?))?;
2270    }
2271    writer.write_all(b"0000")?;
2272    Ok(())
2273}
2274
2275pub fn parse_protocol_v2_command_request(
2276    frames: &[PktLineFrame],
2277) -> Result<ProtocolV2CommandRequest> {
2278    let Some((first, rest)) = frames.split_first() else {
2279        return Err(GitError::InvalidFormat(
2280            "protocol v2 command request is empty".into(),
2281        ));
2282    };
2283    let command = match first {
2284        PktLineFrame::Data(payload) => parse_protocol_v2_command_line(payload)?,
2285        _ => {
2286            return Err(GitError::InvalidFormat(
2287                "protocol v2 command request must start with a command line".into(),
2288            ));
2289        }
2290    };
2291
2292    let mut capabilities = Vec::new();
2293    let mut arguments = Vec::new();
2294    let mut in_arguments = false;
2295    let mut saw_flush = false;
2296    for (idx, frame) in rest.iter().enumerate() {
2297        match frame {
2298            PktLineFrame::Data(payload) if !in_arguments => {
2299                if saw_flush {
2300                    return Err(GitError::InvalidFormat(
2301                        "protocol v2 command request has data after flush".into(),
2302                    ));
2303                }
2304                capabilities.push(parse_protocol_v2_capability_line(payload)?);
2305            }
2306            PktLineFrame::Data(payload) => {
2307                if saw_flush {
2308                    return Err(GitError::InvalidFormat(
2309                        "protocol v2 command request has data after flush".into(),
2310                    ));
2311                }
2312                let argument = trim_trailing_lf(payload);
2313                if argument.is_empty() {
2314                    return Err(GitError::InvalidFormat(
2315                        "protocol v2 command argument is empty".into(),
2316                    ));
2317                }
2318                if argument
2319                    .iter()
2320                    .any(|byte| matches!(*byte, b'\n' | b'\r' | 0))
2321                {
2322                    return Err(GitError::InvalidFormat(
2323                        "protocol v2 command argument contains a delimiter byte".into(),
2324                    ));
2325                }
2326                arguments.push(argument.to_vec());
2327            }
2328            PktLineFrame::Delimiter => {
2329                if in_arguments {
2330                    return Err(GitError::InvalidFormat(
2331                        "protocol v2 command request has duplicate delimiter".into(),
2332                    ));
2333                }
2334                if saw_flush {
2335                    return Err(GitError::InvalidFormat(
2336                        "protocol v2 command request has delimiter after flush".into(),
2337                    ));
2338                }
2339                in_arguments = true;
2340            }
2341            PktLineFrame::Flush => {
2342                saw_flush = true;
2343                if idx + 1 != rest.len() {
2344                    return Err(GitError::InvalidFormat(
2345                        "protocol v2 command request has frames after flush".into(),
2346                    ));
2347                }
2348            }
2349            PktLineFrame::ResponseEnd => {
2350                return Err(GitError::InvalidFormat(
2351                    "protocol v2 command request contains response-end".into(),
2352                ));
2353            }
2354        }
2355    }
2356    if !saw_flush {
2357        return Err(GitError::InvalidFormat(
2358            "protocol v2 command request missing flush".into(),
2359        ));
2360    }
2361
2362    Ok(ProtocolV2CommandRequest {
2363        command,
2364        capabilities,
2365        arguments,
2366    })
2367}
2368
2369pub fn encode_protocol_v2_command_request(
2370    request: &ProtocolV2CommandRequest,
2371) -> Result<Vec<PktLineFrame>> {
2372    validate_capability_name(&request.command)?;
2373    let mut frames = Vec::new();
2374    frames.push(PktLineFrame::data(line_from_str(&format!(
2375        "command={}",
2376        request.command
2377    )))?);
2378    for capability in &request.capabilities {
2379        frames.push(PktLineFrame::data(line(encode_protocol_v2_capability(
2380            capability,
2381        )?))?);
2382    }
2383    if !request.arguments.is_empty() {
2384        frames.push(PktLineFrame::Delimiter);
2385        for argument in &request.arguments {
2386            validate_protocol_v2_argument(argument)?;
2387            let mut payload = argument.clone();
2388            payload.push(b'\n');
2389            frames.push(PktLineFrame::data(payload)?);
2390        }
2391    }
2392    frames.push(PktLineFrame::Flush);
2393    Ok(frames)
2394}
2395
2396pub fn parse_protocol_v2_request(frames: &[PktLineFrame]) -> Result<ProtocolV2Request> {
2397    if matches!(frames, [PktLineFrame::Flush]) {
2398        return Ok(ProtocolV2Request::Done);
2399    }
2400    parse_protocol_v2_command_request(frames).map(ProtocolV2Request::Command)
2401}
2402
2403pub fn encode_protocol_v2_request(request: &ProtocolV2Request) -> Result<Vec<PktLineFrame>> {
2404    match request {
2405        ProtocolV2Request::Command(command) => encode_protocol_v2_command_request(command),
2406        ProtocolV2Request::Done => Ok(vec![PktLineFrame::Flush]),
2407    }
2408}
2409
2410pub fn read_protocol_v2_request(reader: &mut impl Read) -> Result<ProtocolV2Request> {
2411    let frames = read_pkt_line_frames_until_flush(reader)?;
2412    parse_protocol_v2_request(&frames)
2413}
2414
2415pub fn write_protocol_v2_request(
2416    writer: &mut impl Write,
2417    request: &ProtocolV2Request,
2418) -> Result<()> {
2419    match request {
2420        ProtocolV2Request::Command(command) => write_protocol_v2_command_request(writer, command),
2421        ProtocolV2Request::Done => {
2422            writer.write_all(b"0000")?;
2423            Ok(())
2424        }
2425    }
2426}
2427
2428pub fn read_protocol_v2_command_request(
2429    reader: &mut impl Read,
2430) -> Result<ProtocolV2CommandRequest> {
2431    let frames = read_pkt_line_frames_until_flush(reader)?;
2432    parse_protocol_v2_command_request(&frames)
2433}
2434
2435pub fn write_protocol_v2_command_request(
2436    writer: &mut impl Write,
2437    request: &ProtocolV2CommandRequest,
2438) -> Result<()> {
2439    validate_capability_name(&request.command)?;
2440    write_pkt_line_payload(
2441        writer,
2442        &line_from_str(&format!("command={}", request.command)),
2443    )?;
2444    for capability in &request.capabilities {
2445        write_pkt_line_payload(writer, &line(encode_protocol_v2_capability(capability)?))?;
2446    }
2447    if !request.arguments.is_empty() {
2448        write_pkt_line_frame(writer, &PktLineFrame::Delimiter)?;
2449        for argument in &request.arguments {
2450            validate_protocol_v2_argument(argument)?;
2451            let mut payload = argument.clone();
2452            payload.push(b'\n');
2453            write_pkt_line_payload(writer, &payload)?;
2454        }
2455    }
2456    writer.write_all(b"0000")?;
2457    Ok(())
2458}
2459
2460pub fn read_protocol_v2_ls_refs_request(reader: &mut impl Read) -> Result<ProtocolV2LsRefsRequest> {
2461    let request = read_protocol_v2_command_request(reader)?;
2462    ProtocolV2LsRefsRequest::from_command_request(&request)
2463}
2464
2465pub fn write_protocol_v2_ls_refs_request(
2466    writer: &mut impl Write,
2467    request: &ProtocolV2LsRefsRequest,
2468) -> Result<()> {
2469    let command = request.to_command_request()?;
2470    write_protocol_v2_command_request(writer, &command)
2471}
2472
2473pub fn parse_protocol_v2_ls_refs_response(
2474    format: ObjectFormat,
2475    frames: &[PktLineFrame],
2476) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2477    let mut records = Vec::new();
2478    let mut saw_flush = false;
2479    for (idx, frame) in frames.iter().enumerate() {
2480        match frame {
2481            PktLineFrame::Data(payload) => {
2482                if saw_flush {
2483                    return Err(GitError::InvalidFormat(
2484                        "ls-refs response has data after flush".into(),
2485                    ));
2486                }
2487                records.push(parse_protocol_v2_ls_refs_line(format, payload)?);
2488            }
2489            PktLineFrame::Flush => {
2490                saw_flush = true;
2491                if !flush_terminates_protocol_v2_response(frames, idx) {
2492                    return Err(GitError::InvalidFormat(
2493                        "ls-refs response has frames after flush".into(),
2494                    ));
2495                }
2496            }
2497            PktLineFrame::ResponseEnd if saw_flush && idx + 1 == frames.len() => {}
2498            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
2499                return Err(GitError::InvalidFormat(
2500                    "ls-refs response contains a non-flush control packet".into(),
2501                ));
2502            }
2503        }
2504    }
2505    if !saw_flush {
2506        return Err(GitError::InvalidFormat(
2507            "ls-refs response missing flush".into(),
2508        ));
2509    }
2510    Ok(records)
2511}
2512
2513pub fn encode_protocol_v2_ls_refs_response(
2514    records: &[ProtocolV2LsRefsRecord],
2515) -> Result<Vec<PktLineFrame>> {
2516    let mut frames = Vec::new();
2517    for record in records {
2518        frames.push(PktLineFrame::data(line_from_str(
2519            &format_protocol_v2_ls_refs_record(record)?,
2520        ))?);
2521    }
2522    frames.push(PktLineFrame::Flush);
2523    Ok(frames)
2524}
2525
2526pub fn read_protocol_v2_ls_refs_response(
2527    format: ObjectFormat,
2528    reader: &mut impl Read,
2529) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2530    let frames = read_pkt_line_frames_until_flush(reader)?;
2531    parse_protocol_v2_ls_refs_response(format, &frames)
2532}
2533
2534pub fn write_protocol_v2_ls_refs_response(
2535    writer: &mut impl Write,
2536    records: &[ProtocolV2LsRefsRecord],
2537) -> Result<()> {
2538    for record in records {
2539        write_pkt_line_payload(
2540            writer,
2541            &line_from_str(&format_protocol_v2_ls_refs_record(record)?),
2542        )?;
2543    }
2544    writer.write_all(b"0000")?;
2545    Ok(())
2546}
2547
2548pub fn read_protocol_v2_ls_refs_response_until_response_end(
2549    format: ObjectFormat,
2550    reader: &mut impl Read,
2551) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2552    let frames = read_pkt_line_frames_until_response_end(reader)?;
2553    parse_protocol_v2_ls_refs_response(format, &frames)
2554}
2555
2556pub fn write_protocol_v2_ls_refs_response_with_response_end(
2557    writer: &mut impl Write,
2558    records: &[ProtocolV2LsRefsRecord],
2559) -> Result<()> {
2560    write_protocol_v2_ls_refs_response(writer, records)?;
2561    writer.write_all(b"0002")?;
2562    Ok(())
2563}
2564
2565pub fn exchange_protocol_v2_ls_refs(
2566    format: ObjectFormat,
2567    reader: &mut impl Read,
2568    writer: &mut impl Write,
2569    request: &ProtocolV2LsRefsRequest,
2570) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2571    write_protocol_v2_ls_refs_request(writer, request)?;
2572    writer.flush()?;
2573    read_protocol_v2_ls_refs_response(format, reader)
2574}
2575
2576/// Bridge a parsed protocol v2 `ls-refs` response into the shared
2577/// [`RefAdvertisementSet`]/[`RefAdvertisement`] types used by the v0/v1 codecs,
2578/// so callers can drive v2 clone/fetch through the same ref-advertisement
2579/// machinery.
2580///
2581/// Each [`ProtocolV2LsRefsRecord::Ref`] becomes a [`RefAdvertisement`]. A
2582/// `peeled:<oid>` attribute is emitted as an additional `<peeled-oid>
2583/// <name>^{}` advertisement, matching the v0/v1 peeled-tag convention.
2584/// `symref-target:<target>` attributes are collected as `symref=<name>:<target>`
2585/// capabilities on the first advertised ref, mirroring how the upload-pack v0/v1
2586/// advertisement carries symrefs. [`ProtocolV2LsRefsRecord::Unborn`] records have
2587/// no object id, so they cannot be represented as a [`RefAdvertisement`]; an
2588/// unborn record carrying a `symref-target` is preserved as a `symref` capability
2589/// while otherwise being skipped. The returned set always reports
2590/// [`ProtocolVersion::V2`].
2591pub fn protocol_v2_ls_refs_records_to_ref_advertisement_set(
2592    records: &[ProtocolV2LsRefsRecord],
2593) -> Result<RefAdvertisementSet> {
2594    let mut refs: Vec<RefAdvertisement> = Vec::new();
2595    let mut symrefs: Vec<Capability> = Vec::new();
2596    for record in records {
2597        match record {
2598            ProtocolV2LsRefsRecord::Ref(reference) => {
2599                validate_protocol_v2_token("ls-refs ref name", &reference.name)?;
2600                refs.push(RefAdvertisement {
2601                    oid: reference.oid,
2602                    name: reference.name.clone(),
2603                    capabilities: Vec::new(),
2604                });
2605                if let Some(peeled) = &reference.peeled {
2606                    refs.push(RefAdvertisement {
2607                        oid: peeled.clone(),
2608                        name: format!("{}^{{}}", reference.name),
2609                        capabilities: Vec::new(),
2610                    });
2611                }
2612                if let Some(target) = &reference.symref_target {
2613                    symrefs.push(protocol_v2_symref_capability(&reference.name, target)?);
2614                }
2615            }
2616            ProtocolV2LsRefsRecord::Unborn {
2617                name,
2618                symref_target,
2619                ..
2620            } => {
2621                validate_protocol_v2_token("ls-refs ref name", name)?;
2622                if let Some(target) = symref_target {
2623                    symrefs.push(protocol_v2_symref_capability(name, target)?);
2624                }
2625            }
2626        }
2627    }
2628    if !symrefs.is_empty() {
2629        if let Some(first) = refs.first_mut() {
2630            first.capabilities = symrefs;
2631        } else {
2632            return Err(GitError::InvalidFormat(
2633                "ls-refs response advertised symrefs without any concrete refs".into(),
2634            ));
2635        }
2636    }
2637    Ok(RefAdvertisementSet {
2638        protocol: ProtocolVersion::V2,
2639        refs,
2640        shallow: Vec::new(),
2641    })
2642}
2643
2644/// Parse a protocol v2 `ls-refs` response and bridge it into the shared
2645/// [`RefAdvertisementSet`] type. Convenience wrapper combining
2646/// [`parse_protocol_v2_ls_refs_response`] and
2647/// [`protocol_v2_ls_refs_records_to_ref_advertisement_set`].
2648pub fn parse_protocol_v2_ls_refs_response_as_ref_advertisement_set(
2649    format: ObjectFormat,
2650    frames: &[PktLineFrame],
2651) -> Result<RefAdvertisementSet> {
2652    let records = parse_protocol_v2_ls_refs_response(format, frames)?;
2653    protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
2654}
2655
2656/// Read a protocol v2 `ls-refs` response from `reader` and bridge it into the
2657/// shared [`RefAdvertisementSet`] type.
2658pub fn read_protocol_v2_ls_refs_response_as_ref_advertisement_set(
2659    format: ObjectFormat,
2660    reader: &mut impl Read,
2661) -> Result<RefAdvertisementSet> {
2662    let records = read_protocol_v2_ls_refs_response(format, reader)?;
2663    protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
2664}
2665
2666fn protocol_v2_symref_capability(name: &str, target: &str) -> Result<Capability> {
2667    validate_protocol_v2_token("ls-refs symref-target", target)?;
2668    Ok(Capability {
2669        name: "symref".into(),
2670        value: Some(format!("{name}:{target}")),
2671    })
2672}
2673
2674pub fn read_protocol_v2_fetch_request(
2675    format: ObjectFormat,
2676    reader: &mut impl Read,
2677) -> Result<ProtocolV2FetchRequest> {
2678    let request = read_protocol_v2_command_request(reader)?;
2679    ProtocolV2FetchRequest::from_command_request(format, &request)
2680}
2681
2682pub fn write_protocol_v2_fetch_request(
2683    writer: &mut impl Write,
2684    request: &ProtocolV2FetchRequest,
2685) -> Result<()> {
2686    let command = request.to_command_request()?;
2687    write_protocol_v2_command_request(writer, &command)
2688}
2689
2690pub fn read_protocol_v2_object_info_request(
2691    format: ObjectFormat,
2692    reader: &mut impl Read,
2693) -> Result<ProtocolV2ObjectInfoRequest> {
2694    let request = read_protocol_v2_command_request(reader)?;
2695    ProtocolV2ObjectInfoRequest::from_command_request(format, &request)
2696}
2697
2698pub fn write_protocol_v2_object_info_request(
2699    writer: &mut impl Write,
2700    request: &ProtocolV2ObjectInfoRequest,
2701) -> Result<()> {
2702    let command = request.to_command_request()?;
2703    write_protocol_v2_command_request(writer, &command)
2704}
2705
2706pub fn parse_protocol_v2_fetch_response(
2707    format: ObjectFormat,
2708    frames: &[PktLineFrame],
2709) -> Result<Vec<ProtocolV2FetchResponseSection>> {
2710    let mut sections = Vec::new();
2711    let mut current: Option<(String, Vec<Vec<u8>>)> = None;
2712    let mut saw_flush = false;
2713    for (idx, frame) in frames.iter().enumerate() {
2714        match frame {
2715            PktLineFrame::Data(payload) => {
2716                if saw_flush {
2717                    return Err(GitError::InvalidFormat(
2718                        "fetch response has data after flush".into(),
2719                    ));
2720                }
2721                if let Some((_name, lines)) = &mut current {
2722                    lines.push(payload.clone());
2723                } else {
2724                    let name = parse_fetch_section_header(payload)?;
2725                    current = Some((name, Vec::new()));
2726                }
2727            }
2728            PktLineFrame::Delimiter => {
2729                if saw_flush {
2730                    return Err(GitError::InvalidFormat(
2731                        "fetch response has delimiter after flush".into(),
2732                    ));
2733                }
2734                let Some((name, lines)) = current.take() else {
2735                    return Err(GitError::InvalidFormat(
2736                        "fetch response has delimiter before section".into(),
2737                    ));
2738                };
2739                sections.push(parse_fetch_section(format, name, lines)?);
2740            }
2741            PktLineFrame::Flush => {
2742                saw_flush = true;
2743                if !flush_terminates_protocol_v2_response(frames, idx) {
2744                    return Err(GitError::InvalidFormat(
2745                        "fetch response has frames after flush".into(),
2746                    ));
2747                }
2748                if let Some((name, lines)) = current.take() {
2749                    sections.push(parse_fetch_section(format, name, lines)?);
2750                }
2751            }
2752            PktLineFrame::ResponseEnd if saw_flush && idx + 1 == frames.len() => {}
2753            PktLineFrame::ResponseEnd => {
2754                return Err(GitError::InvalidFormat(
2755                    "fetch response contains response-end".into(),
2756                ));
2757            }
2758        }
2759    }
2760    if !saw_flush {
2761        return Err(GitError::InvalidFormat(
2762            "fetch response missing flush".into(),
2763        ));
2764    }
2765    Ok(sections)
2766}
2767
2768pub fn encode_protocol_v2_fetch_response(
2769    sections: &[ProtocolV2FetchResponseSection],
2770) -> Result<Vec<PktLineFrame>> {
2771    let mut frames = Vec::new();
2772    for (idx, section) in sections.iter().enumerate() {
2773        if idx != 0 {
2774            frames.push(PktLineFrame::Delimiter);
2775        }
2776        frames.push(PktLineFrame::data(line_from_str(
2777            protocol_v2_fetch_section_name(section),
2778        ))?);
2779        for line in format_protocol_v2_fetch_section_lines(section)? {
2780            frames.push(PktLineFrame::data(line)?);
2781        }
2782    }
2783    frames.push(PktLineFrame::Flush);
2784    Ok(frames)
2785}
2786
2787pub fn parse_protocol_v2_fetch_sideband_all_response(
2788    format: ObjectFormat,
2789    frames: &[PktLineFrame],
2790) -> Result<ProtocolV2FetchSidebandAllResponse> {
2791    let mut demuxed = Vec::new();
2792    let mut progress = Vec::new();
2793    let mut in_packfile = false;
2794    for frame in frames {
2795        match frame {
2796            PktLineFrame::Data(payload) if in_packfile => {
2797                demuxed.push(PktLineFrame::Data(payload.clone()));
2798            }
2799            PktLineFrame::Data(payload) => {
2800                let packet = parse_sideband_packet(payload)?;
2801                match packet.channel {
2802                    SideBandChannel::Data => {
2803                        if trim_trailing_lf(&packet.data) == b"packfile" {
2804                            in_packfile = true;
2805                        }
2806                        demuxed.push(PktLineFrame::Data(packet.data));
2807                    }
2808                    SideBandChannel::Progress => progress.push(packet.data),
2809                    SideBandChannel::Fatal => {
2810                        let message = String::from_utf8_lossy(&packet.data).into_owned();
2811                        return Err(GitError::InvalidFormat(format!(
2812                            "sideband fatal: {message}"
2813                        )));
2814                    }
2815                }
2816            }
2817            PktLineFrame::Delimiter => {
2818                in_packfile = false;
2819                demuxed.push(PktLineFrame::Delimiter);
2820            }
2821            PktLineFrame::Flush => {
2822                in_packfile = false;
2823                demuxed.push(PktLineFrame::Flush);
2824            }
2825            PktLineFrame::ResponseEnd => {
2826                in_packfile = false;
2827                demuxed.push(PktLineFrame::ResponseEnd);
2828            }
2829        }
2830    }
2831    Ok(ProtocolV2FetchSidebandAllResponse {
2832        sections: parse_protocol_v2_fetch_response(format, &demuxed)?,
2833        progress,
2834    })
2835}
2836
2837pub fn encode_protocol_v2_fetch_sideband_all_response(
2838    sections: &[ProtocolV2FetchResponseSection],
2839) -> Result<Vec<PktLineFrame>> {
2840    let frames = encode_protocol_v2_fetch_response(sections)?;
2841    let mut encoded = Vec::new();
2842    let mut in_packfile = false;
2843    for frame in frames {
2844        match frame {
2845            PktLineFrame::Data(payload) if in_packfile => {
2846                encoded.push(PktLineFrame::Data(payload));
2847            }
2848            PktLineFrame::Data(payload) => {
2849                if trim_trailing_lf(&payload) == b"packfile" {
2850                    in_packfile = true;
2851                }
2852                encoded.push(PktLineFrame::data(encode_sideband_packet(
2853                    &SideBandPacket {
2854                        channel: SideBandChannel::Data,
2855                        data: payload,
2856                    },
2857                )?)?);
2858            }
2859            PktLineFrame::Delimiter => {
2860                in_packfile = false;
2861                encoded.push(PktLineFrame::Delimiter);
2862            }
2863            PktLineFrame::Flush => {
2864                in_packfile = false;
2865                encoded.push(PktLineFrame::Flush);
2866            }
2867            PktLineFrame::ResponseEnd => {
2868                in_packfile = false;
2869                encoded.push(PktLineFrame::ResponseEnd);
2870            }
2871        }
2872    }
2873    Ok(encoded)
2874}
2875
2876pub fn read_protocol_v2_fetch_response(
2877    format: ObjectFormat,
2878    reader: &mut impl Read,
2879) -> Result<Vec<ProtocolV2FetchResponseSection>> {
2880    let frames = read_pkt_line_frames_until_flush(reader)?;
2881    parse_protocol_v2_fetch_response(format, &frames)
2882}
2883
2884pub fn write_protocol_v2_fetch_response(
2885    writer: &mut impl Write,
2886    sections: &[ProtocolV2FetchResponseSection],
2887) -> Result<()> {
2888    write_protocol_v2_fetch_response_inner(writer, sections, false, false)
2889}
2890
2891pub fn read_protocol_v2_fetch_sideband_all_response(
2892    format: ObjectFormat,
2893    reader: &mut impl Read,
2894) -> Result<ProtocolV2FetchSidebandAllResponse> {
2895    let frames = read_pkt_line_frames_until_flush(reader)?;
2896    parse_protocol_v2_fetch_sideband_all_response(format, &frames)
2897}
2898
2899pub fn write_protocol_v2_fetch_sideband_all_response(
2900    writer: &mut impl Write,
2901    sections: &[ProtocolV2FetchResponseSection],
2902) -> Result<()> {
2903    write_protocol_v2_fetch_response_inner(writer, sections, true, false)
2904}
2905
2906pub fn read_protocol_v2_fetch_response_until_response_end(
2907    format: ObjectFormat,
2908    reader: &mut impl Read,
2909) -> Result<Vec<ProtocolV2FetchResponseSection>> {
2910    let frames = read_pkt_line_frames_until_response_end(reader)?;
2911    parse_protocol_v2_fetch_response(format, &frames)
2912}
2913
2914pub fn write_protocol_v2_fetch_response_with_response_end(
2915    writer: &mut impl Write,
2916    sections: &[ProtocolV2FetchResponseSection],
2917) -> Result<()> {
2918    write_protocol_v2_fetch_response_inner(writer, sections, false, true)
2919}
2920
2921pub fn read_protocol_v2_fetch_sideband_all_response_until_response_end(
2922    format: ObjectFormat,
2923    reader: &mut impl Read,
2924) -> Result<ProtocolV2FetchSidebandAllResponse> {
2925    let frames = read_pkt_line_frames_until_response_end(reader)?;
2926    parse_protocol_v2_fetch_sideband_all_response(format, &frames)
2927}
2928
2929pub fn write_protocol_v2_fetch_sideband_all_response_with_response_end(
2930    writer: &mut impl Write,
2931    sections: &[ProtocolV2FetchResponseSection],
2932) -> Result<()> {
2933    write_protocol_v2_fetch_response_inner(writer, sections, true, true)
2934}
2935
2936fn write_protocol_v2_fetch_response_inner(
2937    writer: &mut impl Write,
2938    sections: &[ProtocolV2FetchResponseSection],
2939    sideband_all: bool,
2940    response_end: bool,
2941) -> Result<()> {
2942    let mut in_packfile = false;
2943    for (idx, section) in sections.iter().enumerate() {
2944        if idx != 0 {
2945            in_packfile = false;
2946            write_pkt_line_frame(writer, &PktLineFrame::Delimiter)?;
2947        }
2948        write_protocol_v2_fetch_payload(
2949            writer,
2950            &line_from_str(protocol_v2_fetch_section_name(section)),
2951            sideband_all,
2952            &mut in_packfile,
2953        )?;
2954        for payload in format_protocol_v2_fetch_section_lines(section)? {
2955            write_protocol_v2_fetch_payload(writer, &payload, sideband_all, &mut in_packfile)?;
2956        }
2957    }
2958    writer.write_all(b"0000")?;
2959    if response_end {
2960        writer.write_all(b"0002")?;
2961    }
2962    Ok(())
2963}
2964
2965fn write_protocol_v2_fetch_payload(
2966    writer: &mut impl Write,
2967    payload: &[u8],
2968    sideband_all: bool,
2969    in_packfile: &mut bool,
2970) -> Result<()> {
2971    if sideband_all && !*in_packfile {
2972        if trim_trailing_lf(payload) == b"packfile" {
2973            *in_packfile = true;
2974        }
2975        write_sideband_payload(writer, SideBandChannel::Data, payload)
2976    } else {
2977        write_pkt_line_payload(writer, payload)
2978    }
2979}
2980
2981pub fn exchange_protocol_v2_fetch(
2982    format: ObjectFormat,
2983    reader: &mut impl Read,
2984    writer: &mut impl Write,
2985    request: &ProtocolV2FetchRequest,
2986) -> Result<Vec<ProtocolV2FetchResponseSection>> {
2987    write_protocol_v2_fetch_request(writer, request)?;
2988    writer.flush()?;
2989    read_protocol_v2_fetch_response(format, reader)
2990}
2991
2992pub fn parse_protocol_v2_object_info_response(
2993    format: ObjectFormat,
2994    frames: &[PktLineFrame],
2995) -> Result<ProtocolV2ObjectInfoResponse> {
2996    let Some((first, rest)) = frames.split_first() else {
2997        return Err(GitError::InvalidFormat(
2998            "object-info response is empty".into(),
2999        ));
3000    };
3001    let PktLineFrame::Data(attrs) = first else {
3002        return Err(GitError::InvalidFormat(
3003            "object-info response must start with attributes".into(),
3004        ));
3005    };
3006    let attrs = parse_protocol_v2_line_text("object-info response attributes", attrs)?;
3007    let mut response = ProtocolV2ObjectInfoResponse::default();
3008    for attr in attrs.split(' ') {
3009        validate_protocol_v2_token("object-info response attribute", attr)?;
3010        match attr {
3011            "size" => {
3012                if response.size {
3013                    return Err(GitError::InvalidFormat(
3014                        "object-info response has duplicate size attribute".into(),
3015                    ));
3016                }
3017                response.size = true;
3018            }
3019            other => {
3020                return Err(GitError::InvalidFormat(format!(
3021                    "unsupported object-info response attribute {other}"
3022                )));
3023            }
3024        }
3025    }
3026    if !response.size {
3027        return Err(GitError::InvalidFormat(
3028            "object-info response is missing size attribute".into(),
3029        ));
3030    }
3031
3032    let mut saw_flush = false;
3033    for (idx, frame) in rest.iter().enumerate() {
3034        match frame {
3035            PktLineFrame::Data(payload) if !saw_flush => {
3036                response
3037                    .records
3038                    .push(parse_protocol_v2_object_info_record(format, payload)?);
3039            }
3040            PktLineFrame::Data(_) => {
3041                return Err(GitError::InvalidFormat(
3042                    "object-info response has data after flush".into(),
3043                ));
3044            }
3045            PktLineFrame::Flush => {
3046                saw_flush = true;
3047                if idx + 1 != rest.len() {
3048                    return Err(GitError::InvalidFormat(
3049                        "object-info response has frames after flush".into(),
3050                    ));
3051                }
3052            }
3053            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
3054                return Err(GitError::InvalidFormat(
3055                    "object-info response contains a non-flush control packet".into(),
3056                ));
3057            }
3058        }
3059    }
3060    if !saw_flush {
3061        return Err(GitError::InvalidFormat(
3062            "object-info response missing flush".into(),
3063        ));
3064    }
3065    Ok(response)
3066}
3067
3068pub fn encode_protocol_v2_object_info_response(
3069    response: &ProtocolV2ObjectInfoResponse,
3070) -> Result<Vec<PktLineFrame>> {
3071    if !response.size {
3072        return Err(GitError::InvalidFormat(
3073            "object-info response is missing size attribute".into(),
3074        ));
3075    }
3076    let mut frames = Vec::new();
3077    frames.push(PktLineFrame::data(line_from_str("size"))?);
3078    for record in &response.records {
3079        frames.push(PktLineFrame::data(line_from_str(&format!(
3080            "{} {}",
3081            record.oid, record.size
3082        )))?);
3083    }
3084    frames.push(PktLineFrame::Flush);
3085    Ok(frames)
3086}
3087
3088pub fn read_protocol_v2_object_info_response(
3089    format: ObjectFormat,
3090    reader: &mut impl Read,
3091) -> Result<ProtocolV2ObjectInfoResponse> {
3092    let frames = read_pkt_line_frames_until_flush(reader)?;
3093    parse_protocol_v2_object_info_response(format, &frames)
3094}
3095
3096pub fn write_protocol_v2_object_info_response(
3097    writer: &mut impl Write,
3098    response: &ProtocolV2ObjectInfoResponse,
3099) -> Result<()> {
3100    if !response.size {
3101        return Err(GitError::InvalidFormat(
3102            "object-info response is missing size attribute".into(),
3103        ));
3104    }
3105    write_pkt_line_payload(writer, b"size\n")?;
3106    for record in &response.records {
3107        write_pkt_line_payload(
3108            writer,
3109            &line_from_str(&format!("{} {}", record.oid, record.size)),
3110        )?;
3111    }
3112    writer.write_all(b"0000")?;
3113    Ok(())
3114}
3115
3116pub fn exchange_protocol_v2_object_info(
3117    format: ObjectFormat,
3118    reader: &mut impl Read,
3119    writer: &mut impl Write,
3120    request: &ProtocolV2ObjectInfoRequest,
3121) -> Result<ProtocolV2ObjectInfoResponse> {
3122    write_protocol_v2_object_info_request(writer, request)?;
3123    writer.flush()?;
3124    read_protocol_v2_object_info_response(format, reader)
3125}
3126
3127pub fn demux_protocol_v2_fetch_packfile(
3128    sections: &[ProtocolV2FetchResponseSection],
3129) -> Result<Option<SideBandDemux>> {
3130    let mut packfile = None;
3131    for section in sections {
3132        if let ProtocolV2FetchResponseSection::Packfile(lines) = section {
3133            if packfile.is_some() {
3134                return Err(GitError::InvalidFormat(
3135                    "fetch response has duplicate packfile sections".into(),
3136                ));
3137            }
3138            packfile = Some(parse_and_demux_sideband_packets(lines)?);
3139        }
3140    }
3141    Ok(packfile)
3142}
3143
3144pub fn protocol_v2_object_format(capabilities: &[Capability]) -> Result<ObjectFormat> {
3145    let mut format = None;
3146    for capability in capabilities {
3147        if capability.name != "object-format" {
3148            continue;
3149        }
3150        if format.is_some() {
3151            return Err(GitError::InvalidFormat(
3152                "protocol v2 has duplicate object-format capabilities".into(),
3153            ));
3154        }
3155        let Some(value) = &capability.value else {
3156            return Err(GitError::InvalidFormat(
3157                "protocol v2 object-format capability is missing a value".into(),
3158            ));
3159        };
3160        format = Some(value.parse::<ObjectFormat>()?);
3161    }
3162    Ok(format.unwrap_or(ObjectFormat::Sha1))
3163}
3164
3165pub fn validate_protocol_v2_command_request_capabilities(
3166    handshake: &TransportHandshake,
3167    request: &ProtocolV2CommandRequest,
3168) -> Result<()> {
3169    if handshake.protocol != ProtocolVersion::V2 {
3170        return Err(GitError::InvalidFormat(
3171            "protocol v2 command validation requires a v2 handshake".into(),
3172        ));
3173    }
3174    let advertised =
3175        protocol_v2_capability(&handshake.capabilities, &request.command).ok_or_else(|| {
3176            GitError::InvalidFormat(format!("unadvertised command {}", request.command))
3177        })?;
3178    if advertised.name.is_empty() {
3179        return Err(GitError::InvalidFormat(
3180            "advertised command capability is empty".into(),
3181        ));
3182    }
3183    parse_protocol_v2_command_options(&request.capabilities)?;
3184
3185    for capability in &request.capabilities {
3186        let advertised = protocol_v2_capability(&handshake.capabilities, &capability.name)
3187            .ok_or_else(|| {
3188                GitError::InvalidFormat(format!(
3189                    "unadvertised protocol v2 capability {}",
3190                    capability.name
3191                ))
3192            })?;
3193        if capability.name == "object-format" {
3194            validate_protocol_v2_object_format_request(advertised, capability)?;
3195        }
3196    }
3197    Ok(())
3198}
3199
3200pub fn parse_protocol_v2_command_options(
3201    capabilities: &[Capability],
3202) -> Result<ProtocolV2CommandOptions> {
3203    let mut out = ProtocolV2CommandOptions::default();
3204    for capability in capabilities {
3205        match capability.name.as_str() {
3206            "agent" => {
3207                if out.agent.is_some() {
3208                    return Err(GitError::InvalidFormat(
3209                        "protocol v2 command has duplicate agent capabilities".into(),
3210                    ));
3211                }
3212                let Some(value) = &capability.value else {
3213                    return Err(GitError::InvalidFormat(
3214                        "protocol v2 agent capability is missing a value".into(),
3215                    ));
3216                };
3217                validate_protocol_v2_capability_value(value)?;
3218                out.agent = Some(value.clone());
3219            }
3220            "object-format" => {
3221                if out.object_format.is_some() {
3222                    return Err(GitError::InvalidFormat(
3223                        "protocol v2 command has duplicate object-format capabilities".into(),
3224                    ));
3225                }
3226                let Some(value) = &capability.value else {
3227                    return Err(GitError::InvalidFormat(
3228                        "protocol v2 object-format capability is missing a value".into(),
3229                    ));
3230                };
3231                out.object_format = Some(value.parse::<ObjectFormat>()?);
3232            }
3233            "server-option" => {
3234                let Some(value) = &capability.value else {
3235                    return Err(GitError::InvalidFormat(
3236                        "protocol v2 server-option capability is missing a value".into(),
3237                    ));
3238                };
3239                validate_protocol_v2_capability_value(value)?;
3240                out.server_options.push(value.clone());
3241            }
3242            _ => out.extra.push(capability.clone()),
3243        }
3244    }
3245    Ok(out)
3246}
3247
3248pub fn encode_protocol_v2_command_options(
3249    options: &ProtocolV2CommandOptions,
3250) -> Result<Vec<Capability>> {
3251    let mut capabilities = Vec::new();
3252    if let Some(agent) = &options.agent {
3253        validate_protocol_v2_capability_value(agent)?;
3254        capabilities.push(Capability {
3255            name: "agent".into(),
3256            value: Some(agent.clone()),
3257        });
3258    }
3259    if let Some(format) = options.object_format {
3260        capabilities.push(Capability {
3261            name: "object-format".into(),
3262            value: Some(format.name().into()),
3263        });
3264    }
3265    for option in &options.server_options {
3266        validate_protocol_v2_capability_value(option)?;
3267        capabilities.push(Capability {
3268            name: "server-option".into(),
3269            value: Some(option.clone()),
3270        });
3271    }
3272    for capability in &options.extra {
3273        if matches!(
3274            capability.name.as_str(),
3275            "agent" | "object-format" | "server-option"
3276        ) {
3277            return Err(GitError::InvalidFormat(format!(
3278                "protocol v2 extra capability duplicates known capability {}",
3279                capability.name
3280            )));
3281        }
3282        encode_protocol_v2_capability(capability)?;
3283        capabilities.push(capability.clone());
3284    }
3285    Ok(capabilities)
3286}
3287
3288pub fn parse_protocol_v2_ls_refs_features(
3289    capabilities: &[Capability],
3290) -> Result<Option<ProtocolV2LsRefsFeatures>> {
3291    let mut ls_refs = None;
3292    for capability in capabilities {
3293        if capability.name != "ls-refs" {
3294            continue;
3295        }
3296        if ls_refs.is_some() {
3297            return Err(GitError::InvalidFormat(
3298                "protocol v2 has duplicate ls-refs capabilities".into(),
3299            ));
3300        }
3301        ls_refs = Some(parse_protocol_v2_ls_refs_feature_value(
3302            capability.value.as_deref(),
3303        )?);
3304    }
3305    Ok(ls_refs)
3306}
3307
3308pub fn encode_protocol_v2_ls_refs_capability(
3309    features: &ProtocolV2LsRefsFeatures,
3310) -> Result<Capability> {
3311    let mut values = Vec::new();
3312    if features.unborn {
3313        values.push("unborn".to_string());
3314    }
3315    for feature in &features.unknown {
3316        validate_protocol_v2_token("ls-refs feature", feature)?;
3317        if feature == "unborn" {
3318            return Err(GitError::InvalidFormat(
3319                "ls-refs unknown features must not duplicate known feature unborn".into(),
3320            ));
3321        }
3322        values.push(feature.clone());
3323    }
3324    Ok(Capability {
3325        name: "ls-refs".into(),
3326        value: (!values.is_empty()).then(|| values.join(" ")),
3327    })
3328}
3329
3330pub fn validate_protocol_v2_ls_refs_request_features(
3331    features: &ProtocolV2LsRefsFeatures,
3332    request: &ProtocolV2LsRefsRequest,
3333) -> Result<()> {
3334    if request.unborn && !features.unborn {
3335        return Err(GitError::InvalidFormat(
3336            "ls-refs request uses unborn without advertised unborn feature".into(),
3337        ));
3338    }
3339    Ok(())
3340}
3341
3342pub fn validate_protocol_v2_ls_refs_command_request(
3343    handshake: &TransportHandshake,
3344    request: &ProtocolV2CommandRequest,
3345) -> Result<ProtocolV2LsRefsRequest> {
3346    validate_protocol_v2_command_request_capabilities(handshake, request)?;
3347    let ls_refs = ProtocolV2LsRefsRequest::from_command_request(request)?;
3348    let features = parse_protocol_v2_ls_refs_features(&handshake.capabilities)?
3349        .ok_or_else(|| GitError::InvalidFormat("ls-refs command was not advertised".into()))?;
3350    validate_protocol_v2_ls_refs_request_features(&features, &ls_refs)?;
3351    Ok(ls_refs)
3352}
3353
3354pub fn parse_protocol_v2_fetch_features(
3355    capabilities: &[Capability],
3356) -> Result<Option<ProtocolV2FetchFeatures>> {
3357    let mut fetch = None;
3358    for capability in capabilities {
3359        if capability.name != "fetch" {
3360            continue;
3361        }
3362        if fetch.is_some() {
3363            return Err(GitError::InvalidFormat(
3364                "protocol v2 has duplicate fetch capabilities".into(),
3365            ));
3366        }
3367        fetch = Some(parse_protocol_v2_fetch_feature_value(
3368            capability.value.as_deref(),
3369        )?);
3370    }
3371    Ok(fetch)
3372}
3373
3374pub fn encode_protocol_v2_fetch_capability(
3375    features: &ProtocolV2FetchFeatures,
3376) -> Result<Capability> {
3377    let mut values = Vec::new();
3378    if features.shallow {
3379        values.push("shallow".to_string());
3380    }
3381    if features.wait_for_done {
3382        values.push("wait-for-done".to_string());
3383    }
3384    if features.filter {
3385        values.push("filter".to_string());
3386    }
3387    if features.ref_in_want {
3388        values.push("ref-in-want".to_string());
3389    }
3390    if features.sideband_all {
3391        values.push("sideband-all".to_string());
3392    }
3393    if features.packfile_uris {
3394        values.push("packfile-uris".to_string());
3395    }
3396    for feature in &features.unknown {
3397        validate_protocol_v2_token("fetch feature", feature)?;
3398        if matches!(
3399            feature.as_str(),
3400            "shallow"
3401                | "wait-for-done"
3402                | "filter"
3403                | "ref-in-want"
3404                | "sideband-all"
3405                | "packfile-uris"
3406        ) {
3407            return Err(GitError::InvalidFormat(format!(
3408                "fetch unknown features must not duplicate known feature {feature}"
3409            )));
3410        }
3411        values.push(feature.clone());
3412    }
3413    Ok(Capability {
3414        name: "fetch".into(),
3415        value: (!values.is_empty()).then(|| values.join(" ")),
3416    })
3417}
3418
3419pub fn validate_protocol_v2_fetch_request_features(
3420    features: &ProtocolV2FetchFeatures,
3421    request: &ProtocolV2FetchRequest,
3422) -> Result<()> {
3423    if !features.shallow
3424        && (!request.shallow.is_empty()
3425            || request.deepen.is_some()
3426            || request.deepen_since.is_some()
3427            || !request.deepen_not.is_empty()
3428            || request.deepen_relative)
3429    {
3430        return Err(GitError::InvalidFormat(
3431            "fetch request uses shallow/deepen arguments without advertised shallow feature".into(),
3432        ));
3433    }
3434    if !features.filter && request.filter.is_some() {
3435        return Err(GitError::InvalidFormat(
3436            "fetch request uses filter without advertised filter feature".into(),
3437        ));
3438    }
3439    if !features.ref_in_want && !request.want_refs.is_empty() {
3440        return Err(GitError::InvalidFormat(
3441            "fetch request uses want-ref without advertised ref-in-want feature".into(),
3442        ));
3443    }
3444    if !features.sideband_all && request.sideband_all {
3445        return Err(GitError::InvalidFormat(
3446            "fetch request uses sideband-all without advertised sideband-all feature".into(),
3447        ));
3448    }
3449    if !features.packfile_uris && request.packfile_uris.is_some() {
3450        return Err(GitError::InvalidFormat(
3451            "fetch request uses packfile-uris without advertised packfile-uris feature".into(),
3452        ));
3453    }
3454    if !features.wait_for_done && request.wait_for_done {
3455        return Err(GitError::InvalidFormat(
3456            "fetch request uses wait-for-done without advertised wait-for-done feature".into(),
3457        ));
3458    }
3459    Ok(())
3460}
3461
3462pub fn validate_protocol_v2_fetch_command_request(
3463    handshake: &TransportHandshake,
3464    format: ObjectFormat,
3465    request: &ProtocolV2CommandRequest,
3466) -> Result<ProtocolV2FetchRequest> {
3467    validate_protocol_v2_command_request_capabilities(handshake, request)?;
3468    let fetch = ProtocolV2FetchRequest::from_command_request(format, request)?;
3469    let features = parse_protocol_v2_fetch_features(&handshake.capabilities)?
3470        .ok_or_else(|| GitError::InvalidFormat("fetch command was not advertised".into()))?;
3471    validate_protocol_v2_fetch_request_features(&features, &fetch)?;
3472    Ok(fetch)
3473}
3474
3475pub fn validate_protocol_v2_object_info_command_request(
3476    handshake: &TransportHandshake,
3477    format: ObjectFormat,
3478    request: &ProtocolV2CommandRequest,
3479) -> Result<ProtocolV2ObjectInfoRequest> {
3480    validate_protocol_v2_command_request_capabilities(handshake, request)?;
3481    let object_info = ProtocolV2ObjectInfoRequest::from_command_request(format, request)?;
3482    protocol_v2_capability(&handshake.capabilities, "object-info")
3483        .ok_or_else(|| GitError::InvalidFormat("object-info command was not advertised".into()))?;
3484    Ok(object_info)
3485}
3486
3487pub fn classify_protocol_v2_command_request(
3488    handshake: &TransportHandshake,
3489    format: ObjectFormat,
3490    request: &ProtocolV2CommandRequest,
3491) -> Result<ProtocolV2Command> {
3492    match request.command.as_str() {
3493        "ls-refs" => validate_protocol_v2_ls_refs_command_request(handshake, request)
3494            .map(ProtocolV2Command::LsRefs),
3495        "fetch" => validate_protocol_v2_fetch_command_request(handshake, format, request)
3496            .map(ProtocolV2Command::Fetch),
3497        "object-info" => {
3498            validate_protocol_v2_object_info_command_request(handshake, format, request)
3499                .map(ProtocolV2Command::ObjectInfo)
3500        }
3501        _ => {
3502            validate_protocol_v2_command_request_capabilities(handshake, request)?;
3503            Ok(ProtocolV2Command::Unknown(request.clone()))
3504        }
3505    }
3506}
3507
3508pub fn classify_protocol_v2_request(
3509    handshake: &TransportHandshake,
3510    format: ObjectFormat,
3511    request: &ProtocolV2Request,
3512) -> Result<ProtocolV2SessionRequest> {
3513    match request {
3514        ProtocolV2Request::Command(command) => {
3515            classify_protocol_v2_command_request(handshake, format, command)
3516                .map(ProtocolV2SessionRequest::Command)
3517        }
3518        ProtocolV2Request::Done => Ok(ProtocolV2SessionRequest::Done),
3519    }
3520}
3521
3522pub fn read_protocol_v2_session_request(
3523    handshake: &TransportHandshake,
3524    format: ObjectFormat,
3525    reader: &mut impl Read,
3526) -> Result<ProtocolV2SessionRequest> {
3527    let request = read_protocol_v2_request(reader)?;
3528    classify_protocol_v2_request(handshake, format, &request)
3529}
3530
3531fn protocol_v2_capability<'a>(
3532    capabilities: &'a [Capability],
3533    name: &str,
3534) -> Option<&'a Capability> {
3535    capabilities
3536        .iter()
3537        .find(|capability| capability.name == name)
3538}
3539
3540fn validate_protocol_v2_object_format_request(
3541    advertised: &Capability,
3542    requested: &Capability,
3543) -> Result<()> {
3544    let Some(advertised) = &advertised.value else {
3545        return Err(GitError::InvalidFormat(
3546            "advertised object-format capability is missing a value".into(),
3547        ));
3548    };
3549    let Some(requested) = &requested.value else {
3550        return Err(GitError::InvalidFormat(
3551            "requested object-format capability is missing a value".into(),
3552        ));
3553    };
3554    if advertised != requested {
3555        return Err(GitError::InvalidFormat(format!(
3556            "requested object-format {requested} does not match advertised {advertised}"
3557        )));
3558    }
3559    Ok(())
3560}
3561
3562fn parse_protocol_v2_ls_refs_feature_value(
3563    value: Option<&str>,
3564) -> Result<ProtocolV2LsRefsFeatures> {
3565    let mut out = ProtocolV2LsRefsFeatures::default();
3566    let Some(value) = value else {
3567        return Ok(out);
3568    };
3569    if value.is_empty() {
3570        return Err(GitError::InvalidFormat(
3571            "protocol v2 ls-refs capability value is empty".into(),
3572        ));
3573    }
3574    for feature in value.split(' ') {
3575        validate_protocol_v2_token("ls-refs feature", feature)?;
3576        match feature {
3577            "unborn" => out.unborn = true,
3578            other => out.unknown.push(other.to_string()),
3579        }
3580    }
3581    Ok(out)
3582}
3583
3584fn parse_protocol_v2_fetch_feature_value(value: Option<&str>) -> Result<ProtocolV2FetchFeatures> {
3585    let mut out = ProtocolV2FetchFeatures::default();
3586    let Some(value) = value else {
3587        return Ok(out);
3588    };
3589    if value.is_empty() {
3590        return Err(GitError::InvalidFormat(
3591            "protocol v2 fetch capability value is empty".into(),
3592        ));
3593    }
3594    for feature in value.split(' ') {
3595        validate_protocol_v2_token("fetch feature", feature)?;
3596        match feature {
3597            "shallow" => out.shallow = true,
3598            "wait-for-done" => out.wait_for_done = true,
3599            "filter" => out.filter = true,
3600            "ref-in-want" => out.ref_in_want = true,
3601            "sideband-all" => out.sideband_all = true,
3602            "packfile-uris" => out.packfile_uris = true,
3603            other => out.unknown.push(other.to_string()),
3604        }
3605    }
3606    Ok(out)
3607}
3608
3609pub fn parse_capabilities(input: &[u8]) -> Result<Vec<Capability>> {
3610    let input = trim_trailing_lf(input);
3611    if input.is_empty() {
3612        return Ok(Vec::new());
3613    }
3614    let text =
3615        std::str::from_utf8(input).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
3616    text.split(' ')
3617        .map(parse_capability_token)
3618        .collect::<Result<Vec<_>>>()
3619}
3620
3621pub fn encode_capabilities(capabilities: &[Capability]) -> Result<Vec<u8>> {
3622    let mut out = Vec::new();
3623    for (idx, capability) in capabilities.iter().enumerate() {
3624        validate_capability_field("capability name", &capability.name)?;
3625        if idx != 0 {
3626            out.push(b' ');
3627        }
3628        out.extend_from_slice(capability.name.as_bytes());
3629        if let Some(value) = &capability.value {
3630            validate_capability_field("capability value", value)?;
3631            out.push(b'=');
3632            out.extend_from_slice(value.as_bytes());
3633        }
3634    }
3635    Ok(out)
3636}
3637
3638pub fn parse_ref_advertisement(format: ObjectFormat, payload: &[u8]) -> Result<RefAdvertisement> {
3639    let payload = trim_trailing_lf(payload);
3640    let (reference, capabilities) = match payload.iter().position(|byte| *byte == 0) {
3641        Some(idx) => (&payload[..idx], parse_capabilities(&payload[idx + 1..])?),
3642        None => (payload, Vec::new()),
3643    };
3644    let text =
3645        std::str::from_utf8(reference).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
3646    let (oid, name) = text
3647        .split_once(' ')
3648        .ok_or_else(|| GitError::InvalidFormat("advertised ref is missing name".into()))?;
3649    if name.is_empty() {
3650        return Err(GitError::InvalidFormat(
3651            "advertised ref name is empty".into(),
3652        ));
3653    }
3654    Ok(RefAdvertisement {
3655        oid: ObjectId::from_hex(format, oid)?,
3656        name: name.to_string(),
3657        capabilities,
3658    })
3659}
3660
3661pub fn encode_ref_advertisement(advertisement: &RefAdvertisement) -> Result<Vec<u8>> {
3662    validate_protocol_v2_token("advertised ref name", &advertisement.name)?;
3663    let mut out = advertisement.oid.to_string().into_bytes();
3664    out.push(b' ');
3665    out.extend_from_slice(advertisement.name.as_bytes());
3666    if !advertisement.capabilities.is_empty() {
3667        out.push(0);
3668        out.extend_from_slice(&encode_capabilities(&advertisement.capabilities)?);
3669    }
3670    out.push(b'\n');
3671    Ok(out)
3672}
3673
3674pub fn parse_ref_advertisements(
3675    format: ObjectFormat,
3676    frames: &[PktLineFrame],
3677) -> Result<Vec<RefAdvertisement>> {
3678    Ok(parse_ref_advertisement_set(format, frames)?.refs)
3679}
3680
3681pub fn parse_ref_advertisement_set(
3682    format: ObjectFormat,
3683    frames: &[PktLineFrame],
3684) -> Result<RefAdvertisementSet> {
3685    let mut set = RefAdvertisementSet {
3686        protocol: ProtocolVersion::V0,
3687        refs: Vec::new(),
3688        shallow: Vec::new(),
3689    };
3690    let mut saw_flush = false;
3691    let mut in_shallow = false;
3692    for (idx, frame) in frames.iter().enumerate() {
3693        match frame {
3694            PktLineFrame::Data(payload) if !saw_flush => {
3695                let trimmed = trim_trailing_lf(payload);
3696                if trimmed == b"version 1" {
3697                    if idx != 0 {
3698                        return Err(GitError::InvalidFormat(
3699                            "advertised ref protocol version must be the first line".into(),
3700                        ));
3701                    }
3702                    set.protocol = ProtocolVersion::V1;
3703                    continue;
3704                }
3705                if trimmed.starts_with(b"version ") {
3706                    return Err(GitError::InvalidFormat(
3707                        "unsupported advertised ref protocol version".into(),
3708                    ));
3709                }
3710                if trimmed.starts_with(b"shallow ") {
3711                    if set.refs.is_empty() {
3712                        return Err(GitError::InvalidFormat(
3713                            "advertised shallow refs must follow advertised refs".into(),
3714                        ));
3715                    }
3716                    let text = parse_protocol_v2_line_text("advertised shallow ref", payload)?;
3717                    set.shallow.push(parse_oid_argument(
3718                        format,
3719                        "advertised shallow ref",
3720                        text,
3721                        "shallow ",
3722                    )?);
3723                    in_shallow = true;
3724                    continue;
3725                }
3726                if in_shallow {
3727                    return Err(GitError::InvalidFormat(
3728                        "advertised refs must not follow shallow refs".into(),
3729                    ));
3730                }
3731                let advertisement = parse_ref_advertisement(format, payload)?;
3732                if !set.refs.is_empty() && !advertisement.capabilities.is_empty() {
3733                    return Err(GitError::InvalidFormat(
3734                        "advertised ref capabilities must appear on the first ref".into(),
3735                    ));
3736                }
3737                set.refs.push(advertisement);
3738            }
3739            PktLineFrame::Data(_) => {
3740                return Err(GitError::InvalidFormat(
3741                    "advertised ref stream has data after flush".into(),
3742                ));
3743            }
3744            PktLineFrame::Flush => {
3745                saw_flush = true;
3746                if idx + 1 != frames.len() {
3747                    return Err(GitError::InvalidFormat(
3748                        "advertised ref stream has frames after flush".into(),
3749                    ));
3750                }
3751            }
3752            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
3753                return Err(GitError::InvalidFormat(
3754                    "advertised ref stream contains a non-flush control packet".into(),
3755                ));
3756            }
3757        }
3758    }
3759    if !saw_flush {
3760        return Err(GitError::InvalidFormat(
3761            "advertised ref stream missing flush".into(),
3762        ));
3763    }
3764    Ok(set)
3765}
3766
3767pub fn encode_ref_advertisements(advertisements: &[RefAdvertisement]) -> Result<Vec<PktLineFrame>> {
3768    encode_ref_advertisement_set(&RefAdvertisementSet {
3769        protocol: ProtocolVersion::V0,
3770        refs: advertisements.to_vec(),
3771        shallow: Vec::new(),
3772    })
3773}
3774
3775pub fn encode_ref_advertisement_set(set: &RefAdvertisementSet) -> Result<Vec<PktLineFrame>> {
3776    let mut frames = Vec::new();
3777    match set.protocol {
3778        ProtocolVersion::V0 => {}
3779        ProtocolVersion::V1 => frames.push(PktLineFrame::data(line_from_str("version 1"))?),
3780        ProtocolVersion::V2 => {
3781            return Err(GitError::InvalidFormat(
3782                "protocol v2 does not use v0/v1 advertised-ref streams".into(),
3783            ));
3784        }
3785    }
3786    if set.refs.is_empty() && !set.shallow.is_empty() {
3787        return Err(GitError::InvalidFormat(
3788            "advertised shallow refs require advertised refs".into(),
3789        ));
3790    }
3791    for (idx, advertisement) in set.refs.iter().enumerate() {
3792        if idx != 0 && !advertisement.capabilities.is_empty() {
3793            return Err(GitError::InvalidFormat(
3794                "advertised ref capabilities must appear on the first ref".into(),
3795            ));
3796        }
3797        frames.push(PktLineFrame::data(encode_ref_advertisement(
3798            advertisement,
3799        )?)?);
3800    }
3801    for oid in &set.shallow {
3802        frames.push(PktLineFrame::data(line_from_str(&format!(
3803            "shallow {oid}"
3804        )))?);
3805    }
3806    frames.push(PktLineFrame::Flush);
3807    Ok(frames)
3808}
3809
3810pub fn read_ref_advertisements(
3811    format: ObjectFormat,
3812    reader: &mut impl Read,
3813) -> Result<Vec<RefAdvertisement>> {
3814    let frames = read_pkt_line_frames_until_flush(reader)?;
3815    parse_ref_advertisements(format, &frames)
3816}
3817
3818pub fn read_ref_advertisement_set(
3819    format: ObjectFormat,
3820    reader: &mut impl Read,
3821) -> Result<RefAdvertisementSet> {
3822    let frames = read_pkt_line_frames_until_flush(reader)?;
3823    parse_ref_advertisement_set(format, &frames)
3824}
3825
3826pub fn write_ref_advertisements(
3827    writer: &mut impl Write,
3828    advertisements: &[RefAdvertisement],
3829) -> Result<()> {
3830    write_ref_advertisement_stream(writer, ProtocolVersion::V0, advertisements, &[])
3831}
3832
3833pub fn write_ref_advertisement_set(
3834    writer: &mut impl Write,
3835    set: &RefAdvertisementSet,
3836) -> Result<()> {
3837    write_ref_advertisement_stream(writer, set.protocol, &set.refs, &set.shallow)
3838}
3839
3840fn write_ref_advertisement_stream(
3841    writer: &mut impl Write,
3842    protocol: ProtocolVersion,
3843    refs: &[RefAdvertisement],
3844    shallow: &[ObjectId],
3845) -> Result<()> {
3846    match protocol {
3847        ProtocolVersion::V0 => {}
3848        ProtocolVersion::V1 => write_pkt_line_payload(writer, b"version 1\n")?,
3849        ProtocolVersion::V2 => {
3850            return Err(GitError::InvalidFormat(
3851                "protocol v2 does not use v0/v1 advertised-ref streams".into(),
3852            ));
3853        }
3854    }
3855    if refs.is_empty() && !shallow.is_empty() {
3856        return Err(GitError::InvalidFormat(
3857            "advertised shallow refs require advertised refs".into(),
3858        ));
3859    }
3860    for (idx, advertisement) in refs.iter().enumerate() {
3861        if idx != 0 && !advertisement.capabilities.is_empty() {
3862            return Err(GitError::InvalidFormat(
3863                "advertised ref capabilities must appear on the first ref".into(),
3864            ));
3865        }
3866        write_pkt_line_payload(writer, &encode_ref_advertisement(advertisement)?)?;
3867    }
3868    for oid in shallow {
3869        write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
3870    }
3871    writer.write_all(b"0000")?;
3872    Ok(())
3873}
3874
3875pub fn parse_dumb_http_info_refs(
3876    format: ObjectFormat,
3877    input: &[u8],
3878) -> Result<Vec<DumbHttpRefRecord>> {
3879    if input.is_empty() {
3880        return Ok(Vec::new());
3881    }
3882    input
3883        .split_inclusive(|byte| *byte == b'\n')
3884        .map(|line| parse_dumb_http_info_ref_record(format, line))
3885        .collect()
3886}
3887
3888pub fn encode_dumb_http_info_refs(records: &[DumbHttpRefRecord]) -> Result<Vec<u8>> {
3889    let mut out = Vec::new();
3890    for record in records {
3891        validate_dumb_http_ref_name(&record.name)?;
3892        out.extend_from_slice(record.oid.to_string().as_bytes());
3893        out.push(b'\t');
3894        out.extend_from_slice(record.name.as_bytes());
3895        if record.peeled {
3896            out.extend_from_slice(b"^{}");
3897        }
3898        out.push(b'\n');
3899    }
3900    Ok(out)
3901}
3902
3903pub fn read_dumb_http_info_refs(
3904    format: ObjectFormat,
3905    reader: &mut impl Read,
3906) -> Result<Vec<DumbHttpRefRecord>> {
3907    let mut input = Vec::new();
3908    reader.read_to_end(&mut input)?;
3909    parse_dumb_http_info_refs(format, &input)
3910}
3911
3912pub fn write_dumb_http_info_refs(
3913    writer: &mut impl Write,
3914    records: &[DumbHttpRefRecord],
3915) -> Result<()> {
3916    for record in records {
3917        validate_dumb_http_ref_name(&record.name)?;
3918        writer.write_all(record.oid.to_string().as_bytes())?;
3919        writer.write_all(b"\t")?;
3920        writer.write_all(record.name.as_bytes())?;
3921        if record.peeled {
3922            writer.write_all(b"^{}")?;
3923        }
3924        writer.write_all(b"\n")?;
3925    }
3926    Ok(())
3927}
3928
3929pub fn parse_dumb_http_alternates(input: &[u8]) -> Result<Vec<String>> {
3930    if input.is_empty() {
3931        return Ok(Vec::new());
3932    }
3933    input
3934        .split_inclusive(|byte| *byte == b'\n')
3935        .map(parse_dumb_http_alternate)
3936        .collect()
3937}
3938
3939pub fn encode_dumb_http_alternates(alternates: &[String]) -> Result<Vec<u8>> {
3940    let mut out = Vec::new();
3941    for alternate in alternates {
3942        validate_dumb_http_alternate(alternate)?;
3943        out.extend_from_slice(alternate.as_bytes());
3944        out.push(b'\n');
3945    }
3946    Ok(out)
3947}
3948
3949pub fn read_dumb_http_alternates(reader: &mut impl Read) -> Result<Vec<String>> {
3950    let mut input = Vec::new();
3951    reader.read_to_end(&mut input)?;
3952    parse_dumb_http_alternates(&input)
3953}
3954
3955pub fn write_dumb_http_alternates(writer: &mut impl Write, alternates: &[String]) -> Result<()> {
3956    for alternate in alternates {
3957        validate_dumb_http_alternate(alternate)?;
3958        writer.write_all(alternate.as_bytes())?;
3959        writer.write_all(b"\n")?;
3960    }
3961    Ok(())
3962}
3963
3964pub fn parse_dumb_http_packs(
3965    format: ObjectFormat,
3966    input: &[u8],
3967) -> Result<Vec<DumbHttpPackRecord>> {
3968    if input.is_empty() {
3969        return Ok(Vec::new());
3970    }
3971    input
3972        .split_inclusive(|byte| *byte == b'\n')
3973        .map(|line| parse_dumb_http_pack_record(format, line))
3974        .collect()
3975}
3976
3977pub fn encode_dumb_http_packs(records: &[DumbHttpPackRecord]) -> Result<Vec<u8>> {
3978    let mut out = Vec::new();
3979    for record in records {
3980        out.extend_from_slice(format!("P pack-{}.pack\n", record.hash).as_bytes());
3981    }
3982    Ok(out)
3983}
3984
3985pub fn read_dumb_http_packs(
3986    format: ObjectFormat,
3987    reader: &mut impl Read,
3988) -> Result<Vec<DumbHttpPackRecord>> {
3989    let mut input = Vec::new();
3990    reader.read_to_end(&mut input)?;
3991    parse_dumb_http_packs(format, &input)
3992}
3993
3994pub fn write_dumb_http_packs(
3995    writer: &mut impl Write,
3996    records: &[DumbHttpPackRecord],
3997) -> Result<()> {
3998    for record in records {
3999        writer.write_all(format!("P pack-{}.pack\n", record.hash).as_bytes())?;
4000    }
4001    Ok(())
4002}
4003
4004pub fn parse_upload_pack_request(
4005    format: ObjectFormat,
4006    frames: &[PktLineFrame],
4007) -> Result<Option<UploadPackRequest>> {
4008    if matches!(frames, [PktLineFrame::Flush]) {
4009        return Ok(None);
4010    }
4011
4012    let mut request = UploadPackRequest::default();
4013    let mut in_options = false;
4014    let mut saw_flush = false;
4015    for (idx, frame) in frames.iter().enumerate() {
4016        match frame {
4017            PktLineFrame::Data(payload) if !saw_flush => {
4018                let text = parse_protocol_v2_line_text("upload-pack request line", payload)?;
4019                if let Some(value) = text.strip_prefix("want ") {
4020                    if in_options {
4021                        return Err(GitError::InvalidFormat(
4022                            "upload-pack request has want after options".into(),
4023                        ));
4024                    }
4025                    let (oid, capabilities) = if request.wants.is_empty() {
4026                        value
4027                            .split_once(' ')
4028                            .map_or((value, None), |(oid, caps)| (oid, Some(caps.as_bytes())))
4029                    } else {
4030                        if value.contains(' ') {
4031                            return Err(GitError::InvalidFormat(
4032                                "additional upload-pack want has capabilities".into(),
4033                            ));
4034                        }
4035                        (value, None)
4036                    };
4037                    validate_protocol_v2_token("upload-pack want", oid)?;
4038                    request.wants.push(ObjectId::from_hex(format, oid)?);
4039                    if let Some(capabilities) = capabilities {
4040                        request.capabilities = parse_capabilities(capabilities)?;
4041                    }
4042                    continue;
4043                }
4044
4045                if request.wants.is_empty() {
4046                    return Err(GitError::InvalidFormat(
4047                        "upload-pack request must start with want".into(),
4048                    ));
4049                }
4050                in_options = true;
4051                if text.starts_with("shallow ") {
4052                    request.shallow.push(parse_oid_argument(
4053                        format,
4054                        "upload-pack shallow",
4055                        text,
4056                        "shallow ",
4057                    )?);
4058                } else if text.starts_with("deepen ") {
4059                    if request.deepen.is_some() {
4060                        return Err(GitError::InvalidFormat(
4061                            "upload-pack request has duplicate deepen".into(),
4062                        ));
4063                    }
4064                    request.deepen =
4065                        Some(parse_u32_argument("upload-pack deepen", text, "deepen ")?);
4066                } else if text.starts_with("deepen-since ") {
4067                    if request.deepen_since.is_some() {
4068                        return Err(GitError::InvalidFormat(
4069                            "upload-pack request has duplicate deepen-since".into(),
4070                        ));
4071                    }
4072                    request.deepen_since = Some(parse_u64_argument(
4073                        "upload-pack deepen-since",
4074                        text,
4075                        "deepen-since ",
4076                    )?);
4077                } else if let Some(name) = text.strip_prefix("deepen-not ") {
4078                    validate_protocol_v2_token("upload-pack deepen-not", name)?;
4079                    request.deepen_not.push(name.to_string());
4080                } else if let Some(filter) = text.strip_prefix("filter ") {
4081                    if request.filter.is_some() {
4082                        return Err(GitError::InvalidFormat(
4083                            "upload-pack request has duplicate filter".into(),
4084                        ));
4085                    }
4086                    validate_protocol_v2_token("upload-pack filter", filter)?;
4087                    request.filter = Some(filter.to_string());
4088                } else {
4089                    return Err(GitError::InvalidFormat(format!(
4090                        "unsupported upload-pack request line {text}"
4091                    )));
4092                }
4093            }
4094            PktLineFrame::Data(_) => {
4095                return Err(GitError::InvalidFormat(
4096                    "upload-pack request has data after flush".into(),
4097                ));
4098            }
4099            PktLineFrame::Flush => {
4100                saw_flush = true;
4101                if idx + 1 != frames.len() {
4102                    return Err(GitError::InvalidFormat(
4103                        "upload-pack request has frames after flush".into(),
4104                    ));
4105                }
4106            }
4107            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4108                return Err(GitError::InvalidFormat(
4109                    "upload-pack request contains a non-flush control packet".into(),
4110                ));
4111            }
4112        }
4113    }
4114    if !saw_flush {
4115        return Err(GitError::InvalidFormat(
4116            "upload-pack request missing flush".into(),
4117        ));
4118    }
4119    if request.wants.is_empty() {
4120        return Err(GitError::InvalidFormat(
4121            "upload-pack request missing want".into(),
4122        ));
4123    }
4124    Ok(Some(request))
4125}
4126
4127pub fn encode_upload_pack_request(
4128    request: Option<&UploadPackRequest>,
4129) -> Result<Vec<PktLineFrame>> {
4130    let Some(request) = request else {
4131        return Ok(vec![PktLineFrame::Flush]);
4132    };
4133    if request.wants.is_empty() {
4134        return Err(GitError::InvalidFormat(
4135            "upload-pack request missing want".into(),
4136        ));
4137    }
4138
4139    let mut frames = Vec::new();
4140    for (idx, oid) in request.wants.iter().enumerate() {
4141        let mut line = format!("want {oid}");
4142        if idx == 0 && !request.capabilities.is_empty() {
4143            line.push(' ');
4144            line.push_str(
4145                &String::from_utf8(encode_capabilities(&request.capabilities)?)
4146                    .map_err(|err| GitError::InvalidFormat(err.to_string()))?,
4147            );
4148        }
4149        frames.push(PktLineFrame::data(line_from_str(&line))?);
4150    }
4151    for oid in &request.shallow {
4152        frames.push(PktLineFrame::data(line_from_str(&format!(
4153            "shallow {oid}"
4154        )))?);
4155    }
4156    if let Some(deepen) = request.deepen {
4157        if deepen == 0 {
4158            return Err(GitError::InvalidFormat(
4159                "upload-pack deepen must be positive".into(),
4160            ));
4161        }
4162        frames.push(PktLineFrame::data(line_from_str(&format!(
4163            "deepen {deepen}"
4164        )))?);
4165    }
4166    if let Some(deepen_since) = request.deepen_since {
4167        frames.push(PktLineFrame::data(line_from_str(&format!(
4168            "deepen-since {deepen_since}"
4169        )))?);
4170    }
4171    for name in &request.deepen_not {
4172        validate_protocol_v2_token("upload-pack deepen-not", name)?;
4173        frames.push(PktLineFrame::data(line_from_str(&format!(
4174            "deepen-not {name}"
4175        )))?);
4176    }
4177    if let Some(filter) = &request.filter {
4178        validate_protocol_v2_token("upload-pack filter", filter)?;
4179        frames.push(PktLineFrame::data(line_from_str(&format!(
4180            "filter {filter}"
4181        )))?);
4182    }
4183    frames.push(PktLineFrame::Flush);
4184    Ok(frames)
4185}
4186
4187pub fn read_upload_pack_request(
4188    format: ObjectFormat,
4189    reader: &mut impl Read,
4190) -> Result<Option<UploadPackRequest>> {
4191    let frames = read_pkt_line_frames_until_flush(reader)?;
4192    parse_upload_pack_request(format, &frames)
4193}
4194
4195pub fn write_upload_pack_request(
4196    writer: &mut impl Write,
4197    request: Option<&UploadPackRequest>,
4198) -> Result<()> {
4199    let Some(request) = request else {
4200        writer.write_all(b"0000")?;
4201        return Ok(());
4202    };
4203    if request.wants.is_empty() {
4204        return Err(GitError::InvalidFormat(
4205            "upload-pack request missing want".into(),
4206        ));
4207    }
4208
4209    for (idx, oid) in request.wants.iter().enumerate() {
4210        let mut line = format!("want {oid}");
4211        if idx == 0 && !request.capabilities.is_empty() {
4212            line.push(' ');
4213            line.push_str(
4214                &String::from_utf8(encode_capabilities(&request.capabilities)?)
4215                    .map_err(|err| GitError::InvalidFormat(err.to_string()))?,
4216            );
4217        }
4218        write_pkt_line_payload(writer, &line_from_str(&line))?;
4219    }
4220    for oid in &request.shallow {
4221        write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
4222    }
4223    if let Some(deepen) = request.deepen {
4224        if deepen == 0 {
4225            return Err(GitError::InvalidFormat(
4226                "upload-pack deepen must be positive".into(),
4227            ));
4228        }
4229        write_pkt_line_payload(writer, &line_from_str(&format!("deepen {deepen}")))?;
4230    }
4231    if let Some(deepen_since) = request.deepen_since {
4232        write_pkt_line_payload(
4233            writer,
4234            &line_from_str(&format!("deepen-since {deepen_since}")),
4235        )?;
4236    }
4237    for name in &request.deepen_not {
4238        validate_protocol_v2_token("upload-pack deepen-not", name)?;
4239        write_pkt_line_payload(writer, &line_from_str(&format!("deepen-not {name}")))?;
4240    }
4241    if let Some(filter) = &request.filter {
4242        validate_protocol_v2_token("upload-pack filter", filter)?;
4243        write_pkt_line_payload(writer, &line_from_str(&format!("filter {filter}")))?;
4244    }
4245    writer.write_all(b"0000")?;
4246    Ok(())
4247}
4248
4249pub fn parse_upload_pack_features(capabilities: &[Capability]) -> Result<UploadPackFeatures> {
4250    let mut features = UploadPackFeatures::default();
4251    for capability in capabilities {
4252        match capability.name.as_str() {
4253            "multi_ack" => set_upload_pack_flag(&mut features.multi_ack, capability)?,
4254            "multi_ack_detailed" => {
4255                set_upload_pack_flag(&mut features.multi_ack_detailed, capability)?
4256            }
4257            "no-done" => set_upload_pack_flag(&mut features.no_done, capability)?,
4258            "thin-pack" => set_upload_pack_flag(&mut features.thin_pack, capability)?,
4259            "side-band" => set_upload_pack_flag(&mut features.side_band, capability)?,
4260            "side-band-64k" => set_upload_pack_flag(&mut features.side_band_64k, capability)?,
4261            "ofs-delta" => set_upload_pack_flag(&mut features.ofs_delta, capability)?,
4262            "shallow" => set_upload_pack_flag(&mut features.shallow, capability)?,
4263            "deepen-since" => set_upload_pack_flag(&mut features.deepen_since, capability)?,
4264            "deepen-not" => set_upload_pack_flag(&mut features.deepen_not, capability)?,
4265            "include-tag" => set_upload_pack_flag(&mut features.include_tag, capability)?,
4266            "no-progress" => set_upload_pack_flag(&mut features.no_progress, capability)?,
4267            "allow-tip-sha1-in-want" => {
4268                set_upload_pack_flag(&mut features.allow_tip_sha1_in_want, capability)?
4269            }
4270            "allow-reachable-sha1-in-want" => {
4271                set_upload_pack_flag(&mut features.allow_reachable_sha1_in_want, capability)?
4272            }
4273            "filter" => set_upload_pack_flag(&mut features.filter, capability)?,
4274            "agent" => {
4275                let Some(agent) = &capability.value else {
4276                    return Err(GitError::InvalidFormat(
4277                        "upload-pack agent capability is missing value".into(),
4278                    ));
4279                };
4280                if features.agent.is_some() {
4281                    return Err(GitError::InvalidFormat(
4282                        "upload-pack has duplicate agent capability".into(),
4283                    ));
4284                }
4285                validate_capability_field("upload-pack agent", agent)?;
4286                features.agent = Some(agent.clone());
4287            }
4288            "object-format" => {
4289                let Some(format) = &capability.value else {
4290                    return Err(GitError::InvalidFormat(
4291                        "upload-pack object-format capability is missing value".into(),
4292                    ));
4293                };
4294                if features.object_format.is_some() {
4295                    return Err(GitError::InvalidFormat(
4296                        "upload-pack has duplicate object-format capability".into(),
4297                    ));
4298                }
4299                validate_capability_field("upload-pack object-format", format)?;
4300                features.object_format = Some(format.parse()?);
4301            }
4302            "symref" => {
4303                let Some(symref) = &capability.value else {
4304                    return Err(GitError::InvalidFormat(
4305                        "upload-pack symref capability is missing value".into(),
4306                    ));
4307                };
4308                validate_capability_field("upload-pack symref", symref)?;
4309                features.symrefs.push(symref.clone());
4310            }
4311            _ => {
4312                encode_capabilities(std::slice::from_ref(capability))?;
4313                if features
4314                    .unknown
4315                    .iter()
4316                    .any(|known| known.name == capability.name)
4317                {
4318                    return Err(GitError::InvalidFormat(format!(
4319                        "upload-pack has duplicate {} capability",
4320                        capability.name
4321                    )));
4322                }
4323                features.unknown.push(capability.clone());
4324            }
4325        }
4326    }
4327    Ok(features)
4328}
4329
4330pub fn encode_upload_pack_features(features: &UploadPackFeatures) -> Result<Vec<Capability>> {
4331    let mut capabilities = Vec::new();
4332    push_upload_pack_flag(&mut capabilities, "multi_ack", features.multi_ack);
4333    push_upload_pack_flag(
4334        &mut capabilities,
4335        "multi_ack_detailed",
4336        features.multi_ack_detailed,
4337    );
4338    push_upload_pack_flag(&mut capabilities, "no-done", features.no_done);
4339    push_upload_pack_flag(&mut capabilities, "thin-pack", features.thin_pack);
4340    push_upload_pack_flag(&mut capabilities, "side-band", features.side_band);
4341    push_upload_pack_flag(&mut capabilities, "side-band-64k", features.side_band_64k);
4342    push_upload_pack_flag(&mut capabilities, "ofs-delta", features.ofs_delta);
4343    push_upload_pack_flag(&mut capabilities, "shallow", features.shallow);
4344    push_upload_pack_flag(&mut capabilities, "deepen-since", features.deepen_since);
4345    push_upload_pack_flag(&mut capabilities, "deepen-not", features.deepen_not);
4346    push_upload_pack_flag(&mut capabilities, "include-tag", features.include_tag);
4347    push_upload_pack_flag(&mut capabilities, "no-progress", features.no_progress);
4348    push_upload_pack_flag(
4349        &mut capabilities,
4350        "allow-tip-sha1-in-want",
4351        features.allow_tip_sha1_in_want,
4352    );
4353    push_upload_pack_flag(
4354        &mut capabilities,
4355        "allow-reachable-sha1-in-want",
4356        features.allow_reachable_sha1_in_want,
4357    );
4358    push_upload_pack_flag(&mut capabilities, "filter", features.filter);
4359    if let Some(agent) = &features.agent {
4360        validate_capability_field("upload-pack agent", agent)?;
4361        capabilities.push(Capability {
4362            name: "agent".into(),
4363            value: Some(agent.clone()),
4364        });
4365    }
4366    if let Some(format) = features.object_format {
4367        capabilities.push(Capability {
4368            name: "object-format".into(),
4369            value: Some(format.name().into()),
4370        });
4371    }
4372    for symref in &features.symrefs {
4373        validate_capability_field("upload-pack symref", symref)?;
4374        capabilities.push(Capability {
4375            name: "symref".into(),
4376            value: Some(symref.clone()),
4377        });
4378    }
4379    for capability in &features.unknown {
4380        if is_known_upload_pack_capability(&capability.name) {
4381            return Err(GitError::InvalidFormat(format!(
4382                "upload-pack unknown capability duplicates known capability {}",
4383                capability.name
4384            )));
4385        }
4386        encode_capabilities(std::slice::from_ref(capability))?;
4387        capabilities.push(capability.clone());
4388    }
4389    Ok(capabilities)
4390}
4391
4392pub fn validate_upload_pack_request_features(
4393    features: &UploadPackFeatures,
4394    request: &UploadPackRequest,
4395) -> Result<()> {
4396    for capability in &request.capabilities {
4397        if is_upload_pack_flag_capability(&capability.name) {
4398            reject_capability_value("upload-pack request capability", capability)?;
4399        }
4400        match capability.name.as_str() {
4401            "multi_ack" if !features.multi_ack => {
4402                return Err(GitError::InvalidFormat(
4403                    "upload-pack request uses multi_ack without advertised capability".into(),
4404                ));
4405            }
4406            "multi_ack_detailed" if !features.multi_ack_detailed => {
4407                return Err(GitError::InvalidFormat(
4408                    "upload-pack request uses multi_ack_detailed without advertised capability"
4409                        .into(),
4410                ));
4411            }
4412            "no-done" if !features.no_done => {
4413                return Err(GitError::InvalidFormat(
4414                    "upload-pack request uses no-done without advertised capability".into(),
4415                ));
4416            }
4417            "thin-pack" if !features.thin_pack => {
4418                return Err(GitError::InvalidFormat(
4419                    "upload-pack request uses thin-pack without advertised capability".into(),
4420                ));
4421            }
4422            "side-band" if !features.side_band => {
4423                return Err(GitError::InvalidFormat(
4424                    "upload-pack request uses side-band without advertised capability".into(),
4425                ));
4426            }
4427            "side-band-64k" if !features.side_band_64k => {
4428                return Err(GitError::InvalidFormat(
4429                    "upload-pack request uses side-band-64k without advertised capability".into(),
4430                ));
4431            }
4432            "ofs-delta" if !features.ofs_delta => {
4433                return Err(GitError::InvalidFormat(
4434                    "upload-pack request uses ofs-delta without advertised capability".into(),
4435                ));
4436            }
4437            "include-tag" if !features.include_tag => {
4438                return Err(GitError::InvalidFormat(
4439                    "upload-pack request uses include-tag without advertised capability".into(),
4440                ));
4441            }
4442            "no-progress" if !features.no_progress => {
4443                return Err(GitError::InvalidFormat(
4444                    "upload-pack request uses no-progress without advertised capability".into(),
4445                ));
4446            }
4447            "allow-tip-sha1-in-want" if !features.allow_tip_sha1_in_want => {
4448                return Err(GitError::InvalidFormat(
4449                    "upload-pack request uses allow-tip-sha1-in-want without advertised capability"
4450                        .into(),
4451                ));
4452            }
4453            "allow-reachable-sha1-in-want" if !features.allow_reachable_sha1_in_want => {
4454                return Err(GitError::InvalidFormat(
4455                    "upload-pack request uses allow-reachable-sha1-in-want without advertised capability"
4456                        .into(),
4457                ));
4458            }
4459            "filter" if !features.filter => {
4460                return Err(GitError::InvalidFormat(
4461                    "upload-pack request uses filter capability without advertised capability"
4462                        .into(),
4463                ));
4464            }
4465            "agent" => {
4466                let Some(agent) = &capability.value else {
4467                    return Err(GitError::InvalidFormat(
4468                        "upload-pack request agent capability is missing value".into(),
4469                    ));
4470                };
4471                validate_capability_field("upload-pack request agent", agent)?;
4472            }
4473            "object-format" => {
4474                let Some(format) = &capability.value else {
4475                    return Err(GitError::InvalidFormat(
4476                        "upload-pack request object-format capability is missing value".into(),
4477                    ));
4478                };
4479                let requested_format: ObjectFormat = format.parse()?;
4480                if features.object_format != Some(requested_format) {
4481                    return Err(GitError::InvalidFormat(
4482                        "upload-pack request object-format was not advertised".into(),
4483                    ));
4484                }
4485            }
4486            name if is_known_upload_pack_capability(name) => {}
4487            _ => {
4488                if !features
4489                    .unknown
4490                    .iter()
4491                    .any(|advertised| advertised.name == capability.name)
4492                {
4493                    return Err(GitError::InvalidFormat(format!(
4494                        "upload-pack request uses unadvertised capability {}",
4495                        capability.name
4496                    )));
4497                }
4498            }
4499        }
4500    }
4501
4502    let sideband = request
4503        .capabilities
4504        .iter()
4505        .any(|capability| capability.name == "side-band");
4506    let sideband_64k = request
4507        .capabilities
4508        .iter()
4509        .any(|capability| capability.name == "side-band-64k");
4510    if sideband && sideband_64k {
4511        return Err(GitError::InvalidFormat(
4512            "upload-pack request must not request both side-band and side-band-64k".into(),
4513        ));
4514    }
4515
4516    if !features.shallow && (!request.shallow.is_empty() || request.deepen.is_some()) {
4517        return Err(GitError::InvalidFormat(
4518            "upload-pack request uses shallow/deepen without advertised shallow capability".into(),
4519        ));
4520    }
4521    if !features.deepen_since && request.deepen_since.is_some() {
4522        return Err(GitError::InvalidFormat(
4523            "upload-pack request uses deepen-since without advertised capability".into(),
4524        ));
4525    }
4526    if !features.deepen_not && !request.deepen_not.is_empty() {
4527        return Err(GitError::InvalidFormat(
4528            "upload-pack request uses deepen-not without advertised capability".into(),
4529        ));
4530    }
4531    if !features.filter && request.filter.is_some() {
4532        return Err(GitError::InvalidFormat(
4533            "upload-pack request uses filter without advertised capability".into(),
4534        ));
4535    }
4536    Ok(())
4537}
4538
4539pub fn build_upload_pack_raw_packfile_response<C, B>(
4540    features: &UploadPackFeatures,
4541    request: UploadPackRequest,
4542    haves: impl IntoIterator<Item = ObjectId>,
4543    mut contains_object: C,
4544    mut build_pack: B,
4545) -> Result<UploadPackRawPackfileResponse>
4546where
4547    C: FnMut(&ObjectId) -> Result<bool>,
4548    B: FnMut(Vec<ObjectId>, Vec<ObjectId>) -> Result<Option<Vec<u8>>>,
4549{
4550    validate_upload_pack_request_features(features, &request)?;
4551    for want in &request.wants {
4552        if !contains_object(want)? {
4553            return Err(GitError::InvalidObject(format!(
4554                "upload-pack requested missing object {want}"
4555            )));
4556        }
4557    }
4558    let known_haves = haves
4559        .into_iter()
4560        .filter_map(|oid| match contains_object(&oid) {
4561            Ok(true) => Some(Ok(oid)),
4562            Ok(false) => None,
4563            Err(err) => Some(Err(err)),
4564        })
4565        .collect::<Result<Vec<_>>>()?;
4566    let packfile = build_pack(request.wants, known_haves)?
4567        .ok_or_else(|| GitError::InvalidObject("upload-pack request produced empty pack".into()))?;
4568    Ok(UploadPackRawPackfileResponse {
4569        acknowledgments: vec![UploadPackAcknowledgment::Nak],
4570        packfile,
4571    })
4572}
4573
4574pub fn parse_upload_pack_shallow_update(
4575    format: ObjectFormat,
4576    frames: &[PktLineFrame],
4577) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
4578    let mut entries = Vec::new();
4579    let mut saw_flush = false;
4580    for (idx, frame) in frames.iter().enumerate() {
4581        match frame {
4582            PktLineFrame::Data(payload) if !saw_flush => {
4583                entries.push(parse_fetch_shallow_info(format, payload)?);
4584            }
4585            PktLineFrame::Data(_) => {
4586                return Err(GitError::InvalidFormat(
4587                    "upload-pack shallow update has data after flush".into(),
4588                ));
4589            }
4590            PktLineFrame::Flush => {
4591                saw_flush = true;
4592                if idx + 1 != frames.len() {
4593                    return Err(GitError::InvalidFormat(
4594                        "upload-pack shallow update has frames after flush".into(),
4595                    ));
4596                }
4597            }
4598            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4599                return Err(GitError::InvalidFormat(
4600                    "upload-pack shallow update contains a non-flush control packet".into(),
4601                ));
4602            }
4603        }
4604    }
4605    if !saw_flush {
4606        return Err(GitError::InvalidFormat(
4607            "upload-pack shallow update missing flush".into(),
4608        ));
4609    }
4610    Ok(entries)
4611}
4612
4613pub fn encode_upload_pack_shallow_update(
4614    entries: &[ProtocolV2FetchShallowInfo],
4615) -> Result<Vec<PktLineFrame>> {
4616    let mut frames = Vec::new();
4617    for entry in entries {
4618        let line = match entry {
4619            ProtocolV2FetchShallowInfo::Shallow(oid) => format!("shallow {oid}"),
4620            ProtocolV2FetchShallowInfo::Unshallow(oid) => format!("unshallow {oid}"),
4621        };
4622        frames.push(PktLineFrame::data(line_from_str(&line))?);
4623    }
4624    frames.push(PktLineFrame::Flush);
4625    Ok(frames)
4626}
4627
4628pub fn read_upload_pack_shallow_update(
4629    format: ObjectFormat,
4630    reader: &mut impl Read,
4631) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
4632    let frames = read_pkt_line_frames_until_flush(reader)?;
4633    parse_upload_pack_shallow_update(format, &frames)
4634}
4635
4636pub fn write_upload_pack_shallow_update(
4637    writer: &mut impl Write,
4638    entries: &[ProtocolV2FetchShallowInfo],
4639) -> Result<()> {
4640    for entry in entries {
4641        let line = match entry {
4642            ProtocolV2FetchShallowInfo::Shallow(oid) => format!("shallow {oid}"),
4643            ProtocolV2FetchShallowInfo::Unshallow(oid) => format!("unshallow {oid}"),
4644        };
4645        write_pkt_line_payload(writer, &line_from_str(&line))?;
4646    }
4647    writer.write_all(b"0000")?;
4648    Ok(())
4649}
4650
4651pub fn parse_upload_pack_negotiation_request(
4652    format: ObjectFormat,
4653    frames: &[PktLineFrame],
4654) -> Result<UploadPackNegotiationRequest> {
4655    let mut request = UploadPackNegotiationRequest::default();
4656    let mut terminated = false;
4657    for (idx, frame) in frames.iter().enumerate() {
4658        match frame {
4659            PktLineFrame::Data(payload) if !terminated => {
4660                let text = parse_protocol_v2_line_text("upload-pack negotiation line", payload)?;
4661                if text == "done" {
4662                    request.done = true;
4663                    terminated = true;
4664                    if idx + 1 != frames.len() {
4665                        return Err(GitError::InvalidFormat(
4666                            "upload-pack negotiation has frames after done".into(),
4667                        ));
4668                    }
4669                } else if text.starts_with("have ") {
4670                    request.haves.push(parse_oid_argument(
4671                        format,
4672                        "upload-pack have",
4673                        text,
4674                        "have ",
4675                    )?);
4676                } else {
4677                    return Err(GitError::InvalidFormat(format!(
4678                        "unsupported upload-pack negotiation line {text}"
4679                    )));
4680                }
4681            }
4682            PktLineFrame::Data(_) => {
4683                return Err(GitError::InvalidFormat(
4684                    "upload-pack negotiation has data after terminator".into(),
4685                ));
4686            }
4687            PktLineFrame::Flush => {
4688                terminated = true;
4689                if idx + 1 != frames.len() {
4690                    return Err(GitError::InvalidFormat(
4691                        "upload-pack negotiation has frames after flush".into(),
4692                    ));
4693                }
4694            }
4695            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4696                return Err(GitError::InvalidFormat(
4697                    "upload-pack negotiation contains a non-flush control packet".into(),
4698                ));
4699            }
4700        }
4701    }
4702    if !terminated {
4703        return Err(GitError::InvalidFormat(
4704            "upload-pack negotiation missing terminator".into(),
4705        ));
4706    }
4707    Ok(request)
4708}
4709
4710pub fn encode_upload_pack_negotiation_request(
4711    request: &UploadPackNegotiationRequest,
4712) -> Result<Vec<PktLineFrame>> {
4713    let mut frames = Vec::new();
4714    for oid in &request.haves {
4715        frames.push(PktLineFrame::data(line_from_str(&format!("have {oid}")))?);
4716    }
4717    if request.done {
4718        frames.push(PktLineFrame::data(line_from_str("done"))?);
4719    } else {
4720        frames.push(PktLineFrame::Flush);
4721    }
4722    Ok(frames)
4723}
4724
4725pub fn read_upload_pack_negotiation_request(
4726    format: ObjectFormat,
4727    reader: &mut impl Read,
4728) -> Result<UploadPackNegotiationRequest> {
4729    let mut frames = Vec::new();
4730    loop {
4731        let Some(frame) = read_pkt_line_frame(reader)? else {
4732            return Err(GitError::InvalidFormat(
4733                "pkt-line stream ended before upload-pack negotiation terminator".into(),
4734            ));
4735        };
4736        let done = match &frame {
4737            PktLineFrame::Flush => true,
4738            PktLineFrame::Data(payload) => trim_trailing_lf(payload) == b"done",
4739            _ => false,
4740        };
4741        frames.push(frame);
4742        if done {
4743            return parse_upload_pack_negotiation_request(format, &frames);
4744        }
4745    }
4746}
4747
4748pub fn write_upload_pack_negotiation_request(
4749    writer: &mut impl Write,
4750    request: &UploadPackNegotiationRequest,
4751) -> Result<()> {
4752    for oid in &request.haves {
4753        write_pkt_line_payload(writer, &line_from_str(&format!("have {oid}")))?;
4754    }
4755    if request.done {
4756        write_pkt_line_payload(writer, b"done\n")?;
4757    } else {
4758        writer.write_all(b"0000")?;
4759    }
4760    Ok(())
4761}
4762
4763pub fn parse_upload_pack_acknowledgment(
4764    format: ObjectFormat,
4765    payload: &[u8],
4766) -> Result<UploadPackAcknowledgment> {
4767    let text = parse_protocol_v2_line_text("upload-pack acknowledgment", payload)?;
4768    if text == "NAK" {
4769        return Ok(UploadPackAcknowledgment::Nak);
4770    }
4771    let Some(rest) = text.strip_prefix("ACK ") else {
4772        return Err(GitError::InvalidFormat(format!(
4773            "unsupported upload-pack acknowledgment {text}"
4774        )));
4775    };
4776    let mut fields = rest.split(' ');
4777    let oid = fields
4778        .next()
4779        .ok_or_else(|| GitError::InvalidFormat("upload-pack ACK missing object id".into()))?;
4780    validate_protocol_v2_token("upload-pack ACK", oid)?;
4781    let status = match fields.next() {
4782        None => None,
4783        Some("continue") => Some(UploadPackAckStatus::Continue),
4784        Some("common") => Some(UploadPackAckStatus::Common),
4785        Some("ready") => Some(UploadPackAckStatus::Ready),
4786        Some(other) => {
4787            return Err(GitError::InvalidFormat(format!(
4788                "unsupported upload-pack ACK status {other}"
4789            )));
4790        }
4791    };
4792    if fields.next().is_some() {
4793        return Err(GitError::InvalidFormat(
4794            "upload-pack ACK has too many fields".into(),
4795        ));
4796    }
4797    Ok(UploadPackAcknowledgment::Ack {
4798        oid: ObjectId::from_hex(format, oid)?,
4799        status,
4800    })
4801}
4802
4803pub fn encode_upload_pack_acknowledgment(
4804    acknowledgment: &UploadPackAcknowledgment,
4805) -> Result<Vec<u8>> {
4806    let line = match acknowledgment {
4807        UploadPackAcknowledgment::Nak => "NAK".to_string(),
4808        UploadPackAcknowledgment::Ack { oid, status } => {
4809            let mut line = format!("ACK {oid}");
4810            if let Some(status) = status {
4811                line.push(' ');
4812                line.push_str(match status {
4813                    UploadPackAckStatus::Continue => "continue",
4814                    UploadPackAckStatus::Common => "common",
4815                    UploadPackAckStatus::Ready => "ready",
4816                });
4817            }
4818            line
4819        }
4820    };
4821    Ok(line_from_str(&line))
4822}
4823
4824pub fn read_upload_pack_acknowledgment(
4825    format: ObjectFormat,
4826    reader: &mut impl Read,
4827) -> Result<UploadPackAcknowledgment> {
4828    let Some(frame) = read_pkt_line_frame(reader)? else {
4829        return Err(GitError::InvalidFormat(
4830            "pkt-line stream ended before upload-pack acknowledgment".into(),
4831        ));
4832    };
4833    match frame {
4834        PktLineFrame::Data(payload) => parse_upload_pack_acknowledgment(format, &payload),
4835        _ => Err(GitError::InvalidFormat(
4836            "upload-pack acknowledgment must be a data packet".into(),
4837        )),
4838    }
4839}
4840
4841pub fn write_upload_pack_acknowledgment(
4842    writer: &mut impl Write,
4843    acknowledgment: &UploadPackAcknowledgment,
4844) -> Result<()> {
4845    write_pkt_line_payload(writer, &encode_upload_pack_acknowledgment(acknowledgment)?)
4846}
4847
4848pub fn parse_upload_pack_packfile_response(
4849    format: ObjectFormat,
4850    frames: &[PktLineFrame],
4851) -> Result<UploadPackPackfileResponse> {
4852    let mut response = UploadPackPackfileResponse::default();
4853    let mut in_sideband = false;
4854    let mut saw_flush = false;
4855    for (idx, frame) in frames.iter().enumerate() {
4856        match frame {
4857            PktLineFrame::Data(payload) if !saw_flush => {
4858                if !in_sideband
4859                    && (trim_trailing_lf(payload) == b"NAK" || payload.starts_with(b"ACK "))
4860                {
4861                    response
4862                        .acknowledgments
4863                        .push(parse_upload_pack_acknowledgment(format, payload)?);
4864                    continue;
4865                }
4866                in_sideband = true;
4867                response.sideband.push(parse_sideband_packet(payload)?);
4868            }
4869            PktLineFrame::Data(_) => {
4870                return Err(GitError::InvalidFormat(
4871                    "upload-pack packfile response has data after flush".into(),
4872                ));
4873            }
4874            PktLineFrame::Flush => {
4875                saw_flush = true;
4876                if idx + 1 != frames.len() {
4877                    return Err(GitError::InvalidFormat(
4878                        "upload-pack packfile response has frames after flush".into(),
4879                    ));
4880                }
4881            }
4882            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4883                return Err(GitError::InvalidFormat(
4884                    "upload-pack packfile response contains a non-flush control packet".into(),
4885                ));
4886            }
4887        }
4888    }
4889    if !saw_flush {
4890        return Err(GitError::InvalidFormat(
4891            "upload-pack packfile response missing flush".into(),
4892        ));
4893    }
4894    Ok(response)
4895}
4896
4897pub fn encode_upload_pack_packfile_response(
4898    response: &UploadPackPackfileResponse,
4899) -> Result<Vec<PktLineFrame>> {
4900    let mut frames = Vec::new();
4901    for acknowledgment in &response.acknowledgments {
4902        frames.push(PktLineFrame::data(encode_upload_pack_acknowledgment(
4903            acknowledgment,
4904        )?)?);
4905    }
4906    for packet in &response.sideband {
4907        frames.push(PktLineFrame::data(encode_sideband_packet(packet)?)?);
4908    }
4909    frames.push(PktLineFrame::Flush);
4910    Ok(frames)
4911}
4912
4913pub fn read_upload_pack_packfile_response(
4914    format: ObjectFormat,
4915    reader: &mut impl Read,
4916) -> Result<UploadPackPackfileResponse> {
4917    let frames = read_pkt_line_frames_until_flush(reader)?;
4918    parse_upload_pack_packfile_response(format, &frames)
4919}
4920
4921pub fn write_upload_pack_packfile_response(
4922    writer: &mut impl Write,
4923    response: &UploadPackPackfileResponse,
4924) -> Result<()> {
4925    for acknowledgment in &response.acknowledgments {
4926        write_upload_pack_acknowledgment(writer, acknowledgment)?;
4927    }
4928    for packet in &response.sideband {
4929        write_sideband_packet(writer, packet)?;
4930    }
4931    writer.write_all(b"0000")?;
4932    Ok(())
4933}
4934
4935pub fn demux_upload_pack_packfile_response(
4936    response: &UploadPackPackfileResponse,
4937) -> Result<SideBandDemux> {
4938    demux_sideband_packets(&response.sideband)
4939}
4940
4941pub fn parse_upload_pack_raw_packfile_response(
4942    format: ObjectFormat,
4943    input: &[u8],
4944) -> Result<UploadPackRawPackfileResponse> {
4945    let mut response = UploadPackRawPackfileResponse::default();
4946    let mut offset = 0usize;
4947    while offset < input.len() {
4948        match PktLineFrame::parse(&input[offset..]) {
4949            Ok((PktLineFrame::Data(payload), consumed)) => {
4950                let trimmed = trim_trailing_lf(&payload);
4951                if trimmed == b"NAK" || trimmed.starts_with(b"ACK ") {
4952                    response
4953                        .acknowledgments
4954                        .push(parse_upload_pack_acknowledgment(format, &payload)?);
4955                    offset += consumed;
4956                    continue;
4957                }
4958                return Err(GitError::InvalidFormat(
4959                    "upload-pack raw packfile response has non-ack pkt-line before packfile".into(),
4960                ));
4961            }
4962            Ok((PktLineFrame::Flush | PktLineFrame::Delimiter | PktLineFrame::ResponseEnd, _)) => {
4963                return Err(GitError::InvalidFormat(
4964                    "upload-pack raw packfile response contains a control packet".into(),
4965                ));
4966            }
4967            Err(_) if input[offset..].starts_with(b"PACK") => break,
4968            Err(err) => return Err(err),
4969        }
4970    }
4971    response.packfile = input[offset..].to_vec();
4972    if response.packfile.is_empty() {
4973        return Err(GitError::InvalidFormat(
4974            "upload-pack raw packfile response missing packfile".into(),
4975        ));
4976    }
4977    if !response.packfile.starts_with(b"PACK") {
4978        return Err(GitError::InvalidFormat(
4979            "upload-pack raw packfile response packfile must start with PACK".into(),
4980        ));
4981    }
4982    Ok(response)
4983}
4984
4985pub fn encode_upload_pack_raw_packfile_response(
4986    response: &UploadPackRawPackfileResponse,
4987) -> Result<Vec<u8>> {
4988    if response.packfile.is_empty() {
4989        return Err(GitError::InvalidFormat(
4990            "upload-pack raw packfile response missing packfile".into(),
4991        ));
4992    }
4993    if !response.packfile.starts_with(b"PACK") {
4994        return Err(GitError::InvalidFormat(
4995            "upload-pack raw packfile response packfile must start with PACK".into(),
4996        ));
4997    }
4998    let mut out = Vec::new();
4999    for acknowledgment in &response.acknowledgments {
5000        write_pkt_line_payload(
5001            &mut out,
5002            &encode_upload_pack_acknowledgment(acknowledgment)?,
5003        )?;
5004    }
5005    out.extend_from_slice(&response.packfile);
5006    Ok(out)
5007}
5008
5009pub fn read_upload_pack_raw_packfile_response(
5010    format: ObjectFormat,
5011    reader: &mut impl Read,
5012) -> Result<UploadPackRawPackfileResponse> {
5013    let mut input = Vec::new();
5014    reader.read_to_end(&mut input)?;
5015    parse_upload_pack_raw_packfile_response(format, &input)
5016}
5017
5018pub fn write_upload_pack_raw_packfile_response(
5019    writer: &mut impl Write,
5020    response: &UploadPackRawPackfileResponse,
5021) -> Result<()> {
5022    if response.packfile.is_empty() {
5023        return Err(GitError::InvalidFormat(
5024            "upload-pack raw packfile response missing packfile".into(),
5025        ));
5026    }
5027    if !response.packfile.starts_with(b"PACK") {
5028        return Err(GitError::InvalidFormat(
5029            "upload-pack raw packfile response packfile must start with PACK".into(),
5030        ));
5031    }
5032    for acknowledgment in &response.acknowledgments {
5033        write_upload_pack_acknowledgment(writer, acknowledgment)?;
5034    }
5035    writer.write_all(&response.packfile)?;
5036    Ok(())
5037}
5038
5039/// Parse the smart-HTTP/SSH v0 *shallow-info* section that precedes the packfile
5040/// when the upload-pack request carried `shallow`/`deepen`/`deepen-since`/
5041/// `deepen-not` arguments.
5042///
5043/// The section is zero or more `shallow <oid>` / `unshallow <oid>` pkt-lines
5044/// terminated by a flush-pkt; git always emits it (even empty, as a bare flush)
5045/// when the request was a deepen request, and never emits it otherwise. Returns
5046/// the parsed entries and the number of bytes consumed (through the flush) so the
5047/// caller can continue parsing the trailing acknowledgments + packfile from
5048/// `&input[consumed..]` (see [`parse_upload_pack_shallow_info_and_raw_packfile_response`]).
5049pub fn parse_upload_pack_shallow_info_section(
5050    format: ObjectFormat,
5051    input: &[u8],
5052) -> Result<(Vec<ProtocolV2FetchShallowInfo>, usize)> {
5053    let mut entries = Vec::new();
5054    let mut offset = 0usize;
5055    loop {
5056        let (frame, consumed) = PktLineFrame::parse(&input[offset..])?;
5057        offset += consumed;
5058        match frame {
5059            PktLineFrame::Data(payload) => {
5060                entries.push(parse_fetch_shallow_info(format, &payload)?)
5061            }
5062            PktLineFrame::Flush => return Ok((entries, offset)),
5063            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5064                return Err(GitError::InvalidFormat(
5065                    "upload-pack shallow-info section contains a non-flush control packet".into(),
5066                ));
5067            }
5068        }
5069    }
5070}
5071
5072/// Parse a raw upload-pack response that begins with a *shallow-info* section,
5073/// i.e. the response to a deepen request.
5074///
5075/// This is [`parse_upload_pack_raw_packfile_response`] preceded by the
5076/// shallow-info section ([`parse_upload_pack_shallow_info_section`]): it returns
5077/// the `shallow`/`unshallow` entries the server reported alongside the parsed
5078/// acknowledgments + raw packfile. Use it only when the request carried a
5079/// `shallow`/`deepen`/`deepen-since`/`deepen-not` argument; for a plain (non-deepen)
5080/// request the response has no leading shallow-info section and
5081/// [`parse_upload_pack_raw_packfile_response`] must be used instead.
5082pub fn parse_upload_pack_shallow_info_and_raw_packfile_response(
5083    format: ObjectFormat,
5084    input: &[u8],
5085) -> Result<(
5086    Vec<ProtocolV2FetchShallowInfo>,
5087    UploadPackRawPackfileResponse,
5088)> {
5089    let (shallow, consumed) = parse_upload_pack_shallow_info_section(format, input)?;
5090    let response = parse_upload_pack_raw_packfile_response(format, &input[consumed..])?;
5091    Ok((shallow, response))
5092}
5093
5094/// Read a raw upload-pack response that begins with a *shallow-info* section from
5095/// `reader`, returning the `shallow`/`unshallow` entries and the parsed
5096/// acknowledgments + raw packfile.
5097///
5098/// The reader counterpart of
5099/// [`parse_upload_pack_shallow_info_and_raw_packfile_response`]; see it for when
5100/// this applies.
5101pub fn read_upload_pack_shallow_info_and_raw_packfile_response(
5102    format: ObjectFormat,
5103    reader: &mut impl Read,
5104) -> Result<(
5105    Vec<ProtocolV2FetchShallowInfo>,
5106    UploadPackRawPackfileResponse,
5107)> {
5108    let mut input = Vec::new();
5109    reader.read_to_end(&mut input)?;
5110    parse_upload_pack_shallow_info_and_raw_packfile_response(format, &input)
5111}
5112
5113pub fn parse_receive_pack_request(
5114    format: ObjectFormat,
5115    frames: &[PktLineFrame],
5116) -> Result<ReceivePackRequest> {
5117    let mut request = ReceivePackRequest::default();
5118    let mut saw_command = false;
5119    let mut saw_flush = false;
5120    for (idx, frame) in frames.iter().enumerate() {
5121        match frame {
5122            PktLineFrame::Data(payload) if !saw_flush => {
5123                let payload = trim_trailing_lf(payload);
5124                if payload.is_empty() {
5125                    return Err(GitError::InvalidFormat(
5126                        "receive-pack request line is empty".into(),
5127                    ));
5128                }
5129                if let Some(shallow) = payload.strip_prefix(b"shallow ") {
5130                    if saw_command {
5131                        return Err(GitError::InvalidFormat(
5132                            "receive-pack request has shallow after commands".into(),
5133                        ));
5134                    }
5135                    let shallow = std::str::from_utf8(shallow)
5136                        .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
5137                    validate_protocol_v2_token("receive-pack shallow", shallow)?;
5138                    request.shallow.push(ObjectId::from_hex(format, shallow)?);
5139                    continue;
5140                }
5141
5142                let (command, capabilities) = match payload.iter().position(|byte| *byte == 0) {
5143                    Some(nul) if !saw_command => (
5144                        &payload[..nul],
5145                        Some(parse_capabilities(&payload[nul + 1..])?),
5146                    ),
5147                    Some(_) => {
5148                        return Err(GitError::InvalidFormat(
5149                            "receive-pack capabilities must appear on the first command".into(),
5150                        ));
5151                    }
5152                    None => (payload, None),
5153                };
5154                let command = parse_receive_pack_command(format, command)?;
5155                if let Some(capabilities) = capabilities {
5156                    request.capabilities = capabilities;
5157                }
5158                request.commands.push(command);
5159                saw_command = true;
5160            }
5161            PktLineFrame::Data(_) => {
5162                return Err(GitError::InvalidFormat(
5163                    "receive-pack request has data after flush".into(),
5164                ));
5165            }
5166            PktLineFrame::Flush => {
5167                saw_flush = true;
5168                if idx + 1 != frames.len() {
5169                    return Err(GitError::InvalidFormat(
5170                        "receive-pack request has frames after flush".into(),
5171                    ));
5172                }
5173            }
5174            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5175                return Err(GitError::InvalidFormat(
5176                    "receive-pack request contains a non-flush control packet".into(),
5177                ));
5178            }
5179        }
5180    }
5181    if !saw_flush {
5182        return Err(GitError::InvalidFormat(
5183            "receive-pack request missing flush".into(),
5184        ));
5185    }
5186    if !request.shallow.is_empty() && request.commands.is_empty() {
5187        return Err(GitError::InvalidFormat(
5188            "receive-pack request has shallow lines without commands".into(),
5189        ));
5190    }
5191    Ok(request)
5192}
5193
5194pub fn encode_receive_pack_request(request: &ReceivePackRequest) -> Result<Vec<PktLineFrame>> {
5195    if !request.shallow.is_empty() && request.commands.is_empty() {
5196        return Err(GitError::InvalidFormat(
5197            "receive-pack request has shallow lines without commands".into(),
5198        ));
5199    }
5200
5201    let mut frames = Vec::new();
5202    for oid in &request.shallow {
5203        frames.push(PktLineFrame::data(line_from_str(&format!(
5204            "shallow {oid}"
5205        )))?);
5206    }
5207    for (idx, command) in request.commands.iter().enumerate() {
5208        let mut payload = format_receive_pack_command(command)?;
5209        if idx == 0 && !request.capabilities.is_empty() {
5210            payload.push(0);
5211            payload.extend_from_slice(&encode_capabilities(&request.capabilities)?);
5212        }
5213        payload.push(b'\n');
5214        frames.push(PktLineFrame::data(payload)?);
5215    }
5216    frames.push(PktLineFrame::Flush);
5217    Ok(frames)
5218}
5219
5220pub fn read_receive_pack_request(
5221    format: ObjectFormat,
5222    reader: &mut impl Read,
5223) -> Result<ReceivePackRequest> {
5224    let frames = read_pkt_line_frames_until_flush(reader)?;
5225    parse_receive_pack_request(format, &frames)
5226}
5227
5228pub fn write_receive_pack_request(
5229    writer: &mut impl Write,
5230    request: &ReceivePackRequest,
5231) -> Result<()> {
5232    if !request.shallow.is_empty() && request.commands.is_empty() {
5233        return Err(GitError::InvalidFormat(
5234            "receive-pack request has shallow lines without commands".into(),
5235        ));
5236    }
5237
5238    for oid in &request.shallow {
5239        write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
5240    }
5241    for (idx, command) in request.commands.iter().enumerate() {
5242        let mut payload = format_receive_pack_command(command)?;
5243        if idx == 0 && !request.capabilities.is_empty() {
5244            payload.push(0);
5245            payload.extend_from_slice(&encode_capabilities(&request.capabilities)?);
5246        }
5247        payload.push(b'\n');
5248        write_pkt_line_payload(writer, &payload)?;
5249    }
5250    writer.write_all(b"0000")?;
5251    Ok(())
5252}
5253
5254pub fn parse_receive_pack_push_request(
5255    format: ObjectFormat,
5256    input: &[u8],
5257    has_push_options: bool,
5258) -> Result<ReceivePackPushRequest> {
5259    let (command_frames, consumed) = parse_pkt_line_frames_until_flush_from(input)?;
5260    let commands = parse_receive_pack_request(format, &command_frames)?;
5261    let mut offset = consumed;
5262    let push_options = if has_push_options {
5263        let (push_option_frames, consumed) =
5264            parse_pkt_line_frames_until_flush_from(&input[offset..])?;
5265        offset += consumed;
5266        Some(parse_receive_pack_push_options(&push_option_frames)?)
5267    } else {
5268        None
5269    };
5270    Ok(ReceivePackPushRequest {
5271        commands,
5272        push_options,
5273        packfile: input[offset..].to_vec(),
5274    })
5275}
5276
5277pub fn encode_receive_pack_push_request(request: &ReceivePackPushRequest) -> Result<Vec<u8>> {
5278    let mut out = Vec::new();
5279    write_receive_pack_request(&mut out, &request.commands)?;
5280    if let Some(push_options) = &request.push_options {
5281        write_receive_pack_push_options(&mut out, push_options)?;
5282    }
5283    out.extend_from_slice(&request.packfile);
5284    Ok(out)
5285}
5286
5287pub fn read_receive_pack_push_request(
5288    format: ObjectFormat,
5289    reader: &mut impl Read,
5290    has_push_options: bool,
5291) -> Result<ReceivePackPushRequest> {
5292    let commands = read_receive_pack_request(format, reader)?;
5293    let push_options = if has_push_options {
5294        Some(read_receive_pack_push_options(reader)?)
5295    } else {
5296        None
5297    };
5298    let mut packfile = Vec::new();
5299    reader.read_to_end(&mut packfile)?;
5300    Ok(ReceivePackPushRequest {
5301        commands,
5302        push_options,
5303        packfile,
5304    })
5305}
5306
5307pub fn write_receive_pack_push_request(
5308    writer: &mut impl Write,
5309    request: &ReceivePackPushRequest,
5310) -> Result<()> {
5311    write_receive_pack_request(writer, &request.commands)?;
5312    if let Some(push_options) = &request.push_options {
5313        write_receive_pack_push_options(writer, push_options)?;
5314    }
5315    writer.write_all(&request.packfile)?;
5316    Ok(())
5317}
5318
5319pub fn parse_receive_pack_features(capabilities: &[Capability]) -> Result<ReceivePackFeatures> {
5320    let mut features = ReceivePackFeatures::default();
5321    for capability in capabilities {
5322        match capability.name.as_str() {
5323            "report-status" => {
5324                reject_capability_value("receive-pack report-status", capability)?;
5325                if features.report_status {
5326                    return Err(GitError::InvalidFormat(
5327                        "receive-pack has duplicate report-status capability".into(),
5328                    ));
5329                }
5330                features.report_status = true;
5331            }
5332            "report-status-v2" => {
5333                reject_capability_value("receive-pack report-status-v2", capability)?;
5334                if features.report_status_v2 {
5335                    return Err(GitError::InvalidFormat(
5336                        "receive-pack has duplicate report-status-v2 capability".into(),
5337                    ));
5338                }
5339                features.report_status_v2 = true;
5340            }
5341            "delete-refs" => {
5342                reject_capability_value("receive-pack delete-refs", capability)?;
5343                if features.delete_refs {
5344                    return Err(GitError::InvalidFormat(
5345                        "receive-pack has duplicate delete-refs capability".into(),
5346                    ));
5347                }
5348                features.delete_refs = true;
5349            }
5350            "ofs-delta" => {
5351                reject_capability_value("receive-pack ofs-delta", capability)?;
5352                if features.ofs_delta {
5353                    return Err(GitError::InvalidFormat(
5354                        "receive-pack has duplicate ofs-delta capability".into(),
5355                    ));
5356                }
5357                features.ofs_delta = true;
5358            }
5359            "atomic" => {
5360                reject_capability_value("receive-pack atomic", capability)?;
5361                if features.atomic {
5362                    return Err(GitError::InvalidFormat(
5363                        "receive-pack has duplicate atomic capability".into(),
5364                    ));
5365                }
5366                features.atomic = true;
5367            }
5368            "push-options" => {
5369                reject_capability_value("receive-pack push-options", capability)?;
5370                if features.push_options {
5371                    return Err(GitError::InvalidFormat(
5372                        "receive-pack has duplicate push-options capability".into(),
5373                    ));
5374                }
5375                features.push_options = true;
5376            }
5377            "side-band-64k" => {
5378                reject_capability_value("receive-pack side-band-64k", capability)?;
5379                if features.side_band_64k {
5380                    return Err(GitError::InvalidFormat(
5381                        "receive-pack has duplicate side-band-64k capability".into(),
5382                    ));
5383                }
5384                features.side_band_64k = true;
5385            }
5386            "quiet" => {
5387                reject_capability_value("receive-pack quiet", capability)?;
5388                if features.quiet {
5389                    return Err(GitError::InvalidFormat(
5390                        "receive-pack has duplicate quiet capability".into(),
5391                    ));
5392                }
5393                features.quiet = true;
5394            }
5395            "no-thin" => {
5396                reject_capability_value("receive-pack no-thin", capability)?;
5397                if features.no_thin {
5398                    return Err(GitError::InvalidFormat(
5399                        "receive-pack has duplicate no-thin capability".into(),
5400                    ));
5401                }
5402                features.no_thin = true;
5403            }
5404            "agent" => {
5405                let Some(agent) = &capability.value else {
5406                    return Err(GitError::InvalidFormat(
5407                        "receive-pack agent capability is missing value".into(),
5408                    ));
5409                };
5410                if features.agent.is_some() {
5411                    return Err(GitError::InvalidFormat(
5412                        "receive-pack has duplicate agent capability".into(),
5413                    ));
5414                }
5415                validate_capability_field("receive-pack agent", agent)?;
5416                features.agent = Some(agent.clone());
5417            }
5418            "object-format" => {
5419                let Some(format) = &capability.value else {
5420                    return Err(GitError::InvalidFormat(
5421                        "receive-pack object-format capability is missing value".into(),
5422                    ));
5423                };
5424                if features.object_format.is_some() {
5425                    return Err(GitError::InvalidFormat(
5426                        "receive-pack has duplicate object-format capability".into(),
5427                    ));
5428                }
5429                validate_capability_field("receive-pack object-format", format)?;
5430                features.object_format = Some(format.parse()?);
5431            }
5432            _ => {
5433                encode_capabilities(std::slice::from_ref(capability))?;
5434                if features
5435                    .unknown
5436                    .iter()
5437                    .any(|known| known.name == capability.name)
5438                {
5439                    return Err(GitError::InvalidFormat(format!(
5440                        "receive-pack has duplicate {} capability",
5441                        capability.name
5442                    )));
5443                }
5444                features.unknown.push(capability.clone());
5445            }
5446        }
5447    }
5448    Ok(features)
5449}
5450
5451pub fn encode_receive_pack_features(features: &ReceivePackFeatures) -> Result<Vec<Capability>> {
5452    let mut capabilities = Vec::new();
5453    if features.report_status {
5454        capabilities.push(Capability {
5455            name: "report-status".into(),
5456            value: None,
5457        });
5458    }
5459    if features.report_status_v2 {
5460        capabilities.push(Capability {
5461            name: "report-status-v2".into(),
5462            value: None,
5463        });
5464    }
5465    if features.delete_refs {
5466        capabilities.push(Capability {
5467            name: "delete-refs".into(),
5468            value: None,
5469        });
5470    }
5471    if features.ofs_delta {
5472        capabilities.push(Capability {
5473            name: "ofs-delta".into(),
5474            value: None,
5475        });
5476    }
5477    if features.atomic {
5478        capabilities.push(Capability {
5479            name: "atomic".into(),
5480            value: None,
5481        });
5482    }
5483    if features.push_options {
5484        capabilities.push(Capability {
5485            name: "push-options".into(),
5486            value: None,
5487        });
5488    }
5489    if features.side_band_64k {
5490        capabilities.push(Capability {
5491            name: "side-band-64k".into(),
5492            value: None,
5493        });
5494    }
5495    if features.quiet {
5496        capabilities.push(Capability {
5497            name: "quiet".into(),
5498            value: None,
5499        });
5500    }
5501    if features.no_thin {
5502        capabilities.push(Capability {
5503            name: "no-thin".into(),
5504            value: None,
5505        });
5506    }
5507    if let Some(agent) = &features.agent {
5508        validate_capability_field("receive-pack agent", agent)?;
5509        capabilities.push(Capability {
5510            name: "agent".into(),
5511            value: Some(agent.clone()),
5512        });
5513    }
5514    if let Some(format) = features.object_format {
5515        capabilities.push(Capability {
5516            name: "object-format".into(),
5517            value: Some(format.name().into()),
5518        });
5519    }
5520    for capability in &features.unknown {
5521        if is_known_receive_pack_capability(&capability.name) {
5522            return Err(GitError::InvalidFormat(format!(
5523                "receive-pack unknown capability duplicates known capability {}",
5524                capability.name
5525            )));
5526        }
5527        encode_capabilities(std::slice::from_ref(capability))?;
5528        capabilities.push(capability.clone());
5529    }
5530    Ok(capabilities)
5531}
5532
5533pub fn validate_receive_pack_push_request_features(
5534    features: &ReceivePackFeatures,
5535    request: &ReceivePackPushRequest,
5536) -> Result<()> {
5537    for capability in &request.commands.capabilities {
5538        if matches!(
5539            capability.name.as_str(),
5540            "report-status"
5541                | "report-status-v2"
5542                | "delete-refs"
5543                | "ofs-delta"
5544                | "atomic"
5545                | "push-options"
5546                | "side-band-64k"
5547                | "quiet"
5548                | "no-thin"
5549        ) {
5550            reject_capability_value("receive-pack request capability", capability)?;
5551        }
5552        match capability.name.as_str() {
5553            "report-status" if !features.report_status => {
5554                return Err(GitError::InvalidFormat(
5555                    "receive-pack request uses report-status without advertised capability".into(),
5556                ));
5557            }
5558            "report-status-v2" if !features.report_status_v2 => {
5559                return Err(GitError::InvalidFormat(
5560                    "receive-pack request uses report-status-v2 without advertised capability"
5561                        .into(),
5562                ));
5563            }
5564            "delete-refs" if !features.delete_refs => {
5565                return Err(GitError::InvalidFormat(
5566                    "receive-pack request uses delete-refs without advertised capability".into(),
5567                ));
5568            }
5569            "ofs-delta" if !features.ofs_delta => {
5570                return Err(GitError::InvalidFormat(
5571                    "receive-pack request uses ofs-delta without advertised capability".into(),
5572                ));
5573            }
5574            "atomic" if !features.atomic => {
5575                return Err(GitError::InvalidFormat(
5576                    "receive-pack request uses atomic without advertised capability".into(),
5577                ));
5578            }
5579            "push-options" if !features.push_options => {
5580                return Err(GitError::InvalidFormat(
5581                    "receive-pack request uses push-options without advertised capability".into(),
5582                ));
5583            }
5584            "side-band-64k" if !features.side_band_64k => {
5585                return Err(GitError::InvalidFormat(
5586                    "receive-pack request uses side-band-64k without advertised capability".into(),
5587                ));
5588            }
5589            "quiet" if !features.quiet => {
5590                return Err(GitError::InvalidFormat(
5591                    "receive-pack request uses quiet without advertised capability".into(),
5592                ));
5593            }
5594            "no-thin" => {
5595                return Err(GitError::InvalidFormat(
5596                    "receive-pack request must not request no-thin".into(),
5597                ));
5598            }
5599            "agent" => {
5600                validate_capability_field(
5601                    "receive-pack request agent",
5602                    capability.value.as_deref().unwrap_or_default(),
5603                )?;
5604            }
5605            "object-format" => {
5606                let Some(value) = &capability.value else {
5607                    return Err(GitError::InvalidFormat(
5608                        "receive-pack request object-format capability is missing value".into(),
5609                    ));
5610                };
5611                let requested_format: ObjectFormat = value.parse()?;
5612                if features.object_format != Some(requested_format) {
5613                    return Err(GitError::InvalidFormat(
5614                        "receive-pack request object-format was not advertised".into(),
5615                    ));
5616                }
5617            }
5618            name if is_known_receive_pack_capability(name) => {}
5619            _ => {
5620                if !features
5621                    .unknown
5622                    .iter()
5623                    .any(|advertised| advertised.name == capability.name)
5624                {
5625                    return Err(GitError::InvalidFormat(format!(
5626                        "receive-pack request uses unadvertised capability {}",
5627                        capability.name
5628                    )));
5629                }
5630            }
5631        }
5632    }
5633
5634    let requested_push_options = request
5635        .commands
5636        .capabilities
5637        .iter()
5638        .any(|capability| capability.name == "push-options");
5639    match (requested_push_options, &request.push_options) {
5640        (true, Some(_)) => {}
5641        (true, None) => {
5642            return Err(GitError::InvalidFormat(
5643                "receive-pack request uses push-options without push-options section".into(),
5644            ));
5645        }
5646        (false, Some(_)) => {
5647            return Err(GitError::InvalidFormat(
5648                "receive-pack request has push-options section without requested capability".into(),
5649            ));
5650        }
5651        (false, None) => {}
5652    }
5653
5654    let has_delete = request
5655        .commands
5656        .commands
5657        .iter()
5658        .any(is_receive_pack_delete_command);
5659    if has_delete && !features.delete_refs {
5660        return Err(GitError::InvalidFormat(
5661            "receive-pack request deletes refs without advertised delete-refs capability".into(),
5662        ));
5663    }
5664
5665    let has_update_or_create = request
5666        .commands
5667        .commands
5668        .iter()
5669        .any(|command| !is_receive_pack_delete_command(command));
5670    if has_update_or_create && request.packfile.is_empty() {
5671        return Err(GitError::InvalidFormat(
5672            "receive-pack request with create/update commands is missing packfile".into(),
5673        ));
5674    }
5675    if !has_update_or_create && !request.packfile.is_empty() {
5676        return Err(GitError::InvalidFormat(
5677            "receive-pack delete-only request must not include packfile".into(),
5678        ));
5679    }
5680    Ok(())
5681}
5682
5683pub fn apply_receive_pack_push_request<R, I, C, U, D>(
5684    features: &ReceivePackFeatures,
5685    request: &ReceivePackPushRequest,
5686    mut read_ref: R,
5687    mut install_pack: I,
5688    mut contains_object: C,
5689    mut apply_updates: U,
5690    mut delete_ref: D,
5691) -> Result<ReceivePackReportStatus>
5692where
5693    R: FnMut(&str) -> Result<Option<ObjectId>>,
5694    I: FnMut(&[u8]) -> Result<()>,
5695    C: FnMut(&ObjectId) -> Result<bool>,
5696    U: FnMut(&[ReceivePackCommand]) -> Result<()>,
5697    D: FnMut(&ReceivePackCommand) -> Result<()>,
5698{
5699    validate_receive_pack_push_request_features(features, request)?;
5700
5701    for command in request
5702        .commands
5703        .commands
5704        .iter()
5705        .filter(|command| is_receive_pack_delete_command(command))
5706    {
5707        if !command.old_id.is_null() && read_ref(&command.name)? != Some(command.old_id.clone()) {
5708            return Err(GitError::Transaction(format!(
5709                "expected ref {} to match",
5710                command.name
5711            )));
5712        }
5713    }
5714
5715    let updates = request
5716        .commands
5717        .commands
5718        .iter()
5719        .filter(|command| !is_receive_pack_delete_command(command))
5720        .cloned()
5721        .collect::<Vec<_>>();
5722    if !updates.is_empty() {
5723        install_pack(&request.packfile)?;
5724        for command in &updates {
5725            if !contains_object(&command.new_id)? {
5726                return Err(GitError::InvalidObject(format!(
5727                    "receive-pack packfile did not provide {}",
5728                    command.new_id
5729                )));
5730            }
5731        }
5732        apply_updates(&updates)?;
5733    }
5734
5735    for command in request
5736        .commands
5737        .commands
5738        .iter()
5739        .filter(|command| is_receive_pack_delete_command(command))
5740    {
5741        delete_ref(command)?;
5742    }
5743
5744    Ok(ReceivePackReportStatus {
5745        unpack: ReceivePackUnpackStatus::Ok,
5746        commands: request
5747            .commands
5748            .commands
5749            .iter()
5750            .map(|command| ReceivePackCommandStatus::Ok {
5751                name: command.name.clone(),
5752            })
5753            .collect(),
5754    })
5755}
5756
5757pub fn parse_receive_pack_report_status(
5758    frames: &[PktLineFrame],
5759) -> Result<ReceivePackReportStatus> {
5760    let Some((first, rest)) = frames.split_first() else {
5761        return Err(GitError::InvalidFormat(
5762            "receive-pack report-status is empty".into(),
5763        ));
5764    };
5765    let PktLineFrame::Data(payload) = first else {
5766        return Err(GitError::InvalidFormat(
5767            "receive-pack report-status must start with unpack status".into(),
5768        ));
5769    };
5770    let unpack = parse_receive_pack_unpack_status(payload)?;
5771
5772    let mut commands = Vec::new();
5773    let mut saw_flush = false;
5774    for (idx, frame) in rest.iter().enumerate() {
5775        match frame {
5776            PktLineFrame::Data(payload) if !saw_flush => {
5777                commands.push(parse_receive_pack_command_status(payload)?);
5778            }
5779            PktLineFrame::Data(_) => {
5780                return Err(GitError::InvalidFormat(
5781                    "receive-pack report-status has data after flush".into(),
5782                ));
5783            }
5784            PktLineFrame::Flush => {
5785                saw_flush = true;
5786                if idx + 1 != rest.len() {
5787                    return Err(GitError::InvalidFormat(
5788                        "receive-pack report-status has frames after flush".into(),
5789                    ));
5790                }
5791            }
5792            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5793                return Err(GitError::InvalidFormat(
5794                    "receive-pack report-status contains a non-flush control packet".into(),
5795                ));
5796            }
5797        }
5798    }
5799    if !saw_flush {
5800        return Err(GitError::InvalidFormat(
5801            "receive-pack report-status missing flush".into(),
5802        ));
5803    }
5804    Ok(ReceivePackReportStatus { unpack, commands })
5805}
5806
5807pub fn encode_receive_pack_report_status(
5808    report: &ReceivePackReportStatus,
5809) -> Result<Vec<PktLineFrame>> {
5810    let mut frames = Vec::new();
5811    frames.push(PktLineFrame::data(line_from_str(
5812        &format_receive_pack_unpack_status(&report.unpack)?,
5813    ))?);
5814    for command in &report.commands {
5815        frames.push(PktLineFrame::data(line_from_str(
5816            &format_receive_pack_command_status(command)?,
5817        ))?);
5818    }
5819    frames.push(PktLineFrame::Flush);
5820    Ok(frames)
5821}
5822
5823pub fn read_receive_pack_report_status(reader: &mut impl Read) -> Result<ReceivePackReportStatus> {
5824    let frames = read_pkt_line_frames_until_flush(reader)?;
5825    parse_receive_pack_report_status(&frames)
5826}
5827
5828pub fn write_receive_pack_report_status(
5829    writer: &mut impl Write,
5830    report: &ReceivePackReportStatus,
5831) -> Result<()> {
5832    write_pkt_line_payload(
5833        writer,
5834        &line_from_str(&format_receive_pack_unpack_status(&report.unpack)?),
5835    )?;
5836    for command in &report.commands {
5837        write_pkt_line_payload(
5838            writer,
5839            &line_from_str(&format_receive_pack_command_status(command)?),
5840        )?;
5841    }
5842    writer.write_all(b"0000")?;
5843    Ok(())
5844}
5845
5846pub fn parse_receive_pack_report_status_v2(
5847    format: ObjectFormat,
5848    frames: &[PktLineFrame],
5849) -> Result<ReceivePackReportStatusV2> {
5850    let Some((first, rest)) = frames.split_first() else {
5851        return Err(GitError::InvalidFormat(
5852            "receive-pack report-status-v2 is empty".into(),
5853        ));
5854    };
5855    let PktLineFrame::Data(payload) = first else {
5856        return Err(GitError::InvalidFormat(
5857            "receive-pack report-status-v2 must start with unpack status".into(),
5858        ));
5859    };
5860    let unpack = parse_receive_pack_unpack_status(payload)?;
5861
5862    let mut commands = Vec::new();
5863    let mut saw_flush = false;
5864    for (idx, frame) in rest.iter().enumerate() {
5865        match frame {
5866            PktLineFrame::Data(payload) if !saw_flush => {
5867                let text =
5868                    parse_protocol_v2_line_text("receive-pack report-status-v2 line", payload)?;
5869                if text.starts_with("option ") {
5870                    let Some(ReceivePackCommandStatusV2::Ok { options, .. }) = commands.last_mut()
5871                    else {
5872                        return Err(GitError::InvalidFormat(
5873                            "receive-pack report-status-v2 option without ok status".into(),
5874                        ));
5875                    };
5876                    parse_receive_pack_report_status_v2_option(format, text, options)?;
5877                } else {
5878                    commands.push(parse_receive_pack_command_status_v2(text)?);
5879                }
5880            }
5881            PktLineFrame::Data(_) => {
5882                return Err(GitError::InvalidFormat(
5883                    "receive-pack report-status-v2 has data after flush".into(),
5884                ));
5885            }
5886            PktLineFrame::Flush => {
5887                saw_flush = true;
5888                if idx + 1 != rest.len() {
5889                    return Err(GitError::InvalidFormat(
5890                        "receive-pack report-status-v2 has frames after flush".into(),
5891                    ));
5892                }
5893            }
5894            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5895                return Err(GitError::InvalidFormat(
5896                    "receive-pack report-status-v2 contains a non-flush control packet".into(),
5897                ));
5898            }
5899        }
5900    }
5901    if !saw_flush {
5902        return Err(GitError::InvalidFormat(
5903            "receive-pack report-status-v2 missing flush".into(),
5904        ));
5905    }
5906    Ok(ReceivePackReportStatusV2 { unpack, commands })
5907}
5908
5909pub fn encode_receive_pack_report_status_v2(
5910    report: &ReceivePackReportStatusV2,
5911) -> Result<Vec<PktLineFrame>> {
5912    let mut frames = Vec::new();
5913    frames.push(PktLineFrame::data(line_from_str(
5914        &format_receive_pack_unpack_status(&report.unpack)?,
5915    ))?);
5916    for command in &report.commands {
5917        frames.push(PktLineFrame::data(line_from_str(
5918            &format_receive_pack_command_status_v2(command)?,
5919        ))?);
5920        if let ReceivePackCommandStatusV2::Ok { options, .. } = command {
5921            for option in format_receive_pack_report_status_v2_options(options)? {
5922                frames.push(PktLineFrame::data(line_from_str(&option))?);
5923            }
5924        }
5925    }
5926    frames.push(PktLineFrame::Flush);
5927    Ok(frames)
5928}
5929
5930pub fn read_receive_pack_report_status_v2(
5931    format: ObjectFormat,
5932    reader: &mut impl Read,
5933) -> Result<ReceivePackReportStatusV2> {
5934    let frames = read_pkt_line_frames_until_flush(reader)?;
5935    parse_receive_pack_report_status_v2(format, &frames)
5936}
5937
5938pub fn write_receive_pack_report_status_v2(
5939    writer: &mut impl Write,
5940    report: &ReceivePackReportStatusV2,
5941) -> Result<()> {
5942    write_pkt_line_payload(
5943        writer,
5944        &line_from_str(&format_receive_pack_unpack_status(&report.unpack)?),
5945    )?;
5946    for command in &report.commands {
5947        write_pkt_line_payload(
5948            writer,
5949            &line_from_str(&format_receive_pack_command_status_v2(command)?),
5950        )?;
5951        if let ReceivePackCommandStatusV2::Ok { options, .. } = command {
5952            for option in format_receive_pack_report_status_v2_options(options)? {
5953                write_pkt_line_payload(writer, &line_from_str(&option))?;
5954            }
5955        }
5956    }
5957    writer.write_all(b"0000")?;
5958    Ok(())
5959}
5960
5961pub fn parse_receive_pack_push_options(frames: &[PktLineFrame]) -> Result<Vec<String>> {
5962    let mut options = Vec::new();
5963    let mut saw_flush = false;
5964    for (idx, frame) in frames.iter().enumerate() {
5965        match frame {
5966            PktLineFrame::Data(payload) if !saw_flush => {
5967                let option = trim_trailing_lf(payload);
5968                validate_receive_pack_push_option(option)?;
5969                options.push(
5970                    std::str::from_utf8(option)
5971                        .map_err(|err| GitError::InvalidFormat(err.to_string()))?
5972                        .to_string(),
5973                );
5974            }
5975            PktLineFrame::Data(_) => {
5976                return Err(GitError::InvalidFormat(
5977                    "receive-pack push-options has data after flush".into(),
5978                ));
5979            }
5980            PktLineFrame::Flush => {
5981                saw_flush = true;
5982                if idx + 1 != frames.len() {
5983                    return Err(GitError::InvalidFormat(
5984                        "receive-pack push-options has frames after flush".into(),
5985                    ));
5986                }
5987            }
5988            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5989                return Err(GitError::InvalidFormat(
5990                    "receive-pack push-options contains a non-flush control packet".into(),
5991                ));
5992            }
5993        }
5994    }
5995    if !saw_flush {
5996        return Err(GitError::InvalidFormat(
5997            "receive-pack push-options missing flush".into(),
5998        ));
5999    }
6000    Ok(options)
6001}
6002
6003pub fn encode_receive_pack_push_options(options: &[String]) -> Result<Vec<PktLineFrame>> {
6004    let mut frames = Vec::new();
6005    for option in options {
6006        validate_receive_pack_push_option(option.as_bytes())?;
6007        let mut payload = option.as_bytes().to_vec();
6008        payload.push(b'\n');
6009        frames.push(PktLineFrame::data(payload)?);
6010    }
6011    frames.push(PktLineFrame::Flush);
6012    Ok(frames)
6013}
6014
6015pub fn read_receive_pack_push_options(reader: &mut impl Read) -> Result<Vec<String>> {
6016    let frames = read_pkt_line_frames_until_flush(reader)?;
6017    parse_receive_pack_push_options(&frames)
6018}
6019
6020pub fn write_receive_pack_push_options(writer: &mut impl Write, options: &[String]) -> Result<()> {
6021    for option in options {
6022        validate_receive_pack_push_option(option.as_bytes())?;
6023        let mut payload = option.as_bytes().to_vec();
6024        payload.push(b'\n');
6025        write_pkt_line_payload(writer, &payload)?;
6026    }
6027    writer.write_all(b"0000")?;
6028    Ok(())
6029}
6030
6031fn parse_receive_pack_command(format: ObjectFormat, payload: &[u8]) -> Result<ReceivePackCommand> {
6032    let text =
6033        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6034    let mut fields = text.split(' ');
6035    let old_id = fields
6036        .next()
6037        .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing old id".into()))?;
6038    let new_id = fields
6039        .next()
6040        .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing new id".into()))?;
6041    let name = fields
6042        .next()
6043        .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing ref name".into()))?;
6044    if fields.next().is_some() {
6045        return Err(GitError::InvalidFormat(
6046            "receive-pack command has too many fields".into(),
6047        ));
6048    }
6049    validate_protocol_v2_token("receive-pack old id", old_id)?;
6050    validate_protocol_v2_token("receive-pack new id", new_id)?;
6051    validate_protocol_v2_token("receive-pack ref name", name)?;
6052    Ok(ReceivePackCommand {
6053        old_id: ObjectId::from_hex(format, old_id)?,
6054        new_id: ObjectId::from_hex(format, new_id)?,
6055        name: name.to_string(),
6056    })
6057}
6058
6059fn format_receive_pack_command(command: &ReceivePackCommand) -> Result<Vec<u8>> {
6060    if command.old_id.format() != command.new_id.format() {
6061        return Err(GitError::InvalidObjectId(
6062            "receive-pack command object formats do not match".into(),
6063        ));
6064    }
6065    validate_protocol_v2_token("receive-pack ref name", &command.name)?;
6066    Ok(format!("{} {} {}", command.old_id, command.new_id, command.name).into_bytes())
6067}
6068
6069fn set_upload_pack_flag(value: &mut bool, capability: &Capability) -> Result<()> {
6070    reject_capability_value("upload-pack capability", capability)?;
6071    if *value {
6072        return Err(GitError::InvalidFormat(format!(
6073            "upload-pack has duplicate {} capability",
6074            capability.name
6075        )));
6076    }
6077    *value = true;
6078    Ok(())
6079}
6080
6081fn push_upload_pack_flag(capabilities: &mut Vec<Capability>, name: &str, enabled: bool) {
6082    if enabled {
6083        capabilities.push(Capability {
6084            name: name.into(),
6085            value: None,
6086        });
6087    }
6088}
6089
6090fn is_known_upload_pack_capability(name: &str) -> bool {
6091    matches!(
6092        name,
6093        "multi_ack"
6094            | "multi_ack_detailed"
6095            | "no-done"
6096            | "thin-pack"
6097            | "side-band"
6098            | "side-band-64k"
6099            | "ofs-delta"
6100            | "shallow"
6101            | "deepen-since"
6102            | "deepen-not"
6103            | "include-tag"
6104            | "no-progress"
6105            | "allow-tip-sha1-in-want"
6106            | "allow-reachable-sha1-in-want"
6107            | "filter"
6108            | "agent"
6109            | "object-format"
6110            | "symref"
6111    )
6112}
6113
6114fn is_upload_pack_flag_capability(name: &str) -> bool {
6115    matches!(
6116        name,
6117        "multi_ack"
6118            | "multi_ack_detailed"
6119            | "no-done"
6120            | "thin-pack"
6121            | "side-band"
6122            | "side-band-64k"
6123            | "ofs-delta"
6124            | "shallow"
6125            | "deepen-since"
6126            | "deepen-not"
6127            | "include-tag"
6128            | "no-progress"
6129            | "allow-tip-sha1-in-want"
6130            | "allow-reachable-sha1-in-want"
6131            | "filter"
6132    )
6133}
6134
6135fn reject_capability_value(label: &str, capability: &Capability) -> Result<()> {
6136    if capability.value.is_some() {
6137        return Err(GitError::InvalidFormat(format!(
6138            "{label} must not have value"
6139        )));
6140    }
6141    Ok(())
6142}
6143
6144fn is_known_receive_pack_capability(name: &str) -> bool {
6145    matches!(
6146        name,
6147        "report-status"
6148            | "report-status-v2"
6149            | "delete-refs"
6150            | "ofs-delta"
6151            | "atomic"
6152            | "push-options"
6153            | "side-band-64k"
6154            | "quiet"
6155            | "no-thin"
6156            | "agent"
6157            | "object-format"
6158    )
6159}
6160
6161fn is_receive_pack_delete_command(command: &ReceivePackCommand) -> bool {
6162    command.new_id.is_null()
6163}
6164
6165fn parse_receive_pack_unpack_status(payload: &[u8]) -> Result<ReceivePackUnpackStatus> {
6166    let text = parse_protocol_v2_line_text("receive-pack unpack status", payload)?;
6167    if text == "unpack ok" {
6168        return Ok(ReceivePackUnpackStatus::Ok);
6169    }
6170    let Some(message) = text.strip_prefix("unpack ") else {
6171        return Err(GitError::InvalidFormat(format!(
6172            "unsupported receive-pack unpack status {text}"
6173        )));
6174    };
6175    validate_receive_pack_status_message("receive-pack unpack error", message)?;
6176    Ok(ReceivePackUnpackStatus::Error(message.to_string()))
6177}
6178
6179fn format_receive_pack_unpack_status(status: &ReceivePackUnpackStatus) -> Result<String> {
6180    match status {
6181        ReceivePackUnpackStatus::Ok => Ok("unpack ok".into()),
6182        ReceivePackUnpackStatus::Error(message) => {
6183            validate_receive_pack_status_message("receive-pack unpack error", message)?;
6184            Ok(format!("unpack {message}"))
6185        }
6186    }
6187}
6188
6189fn parse_receive_pack_command_status(payload: &[u8]) -> Result<ReceivePackCommandStatus> {
6190    let text = parse_protocol_v2_line_text("receive-pack command status", payload)?;
6191    if let Some(name) = text.strip_prefix("ok ") {
6192        validate_protocol_v2_token("receive-pack status ref name", name)?;
6193        return Ok(ReceivePackCommandStatus::Ok {
6194            name: name.to_string(),
6195        });
6196    }
6197    let Some(rest) = text.strip_prefix("ng ") else {
6198        return Err(GitError::InvalidFormat(format!(
6199            "unsupported receive-pack command status {text}"
6200        )));
6201    };
6202    let (name, message) = rest
6203        .split_once(' ')
6204        .ok_or_else(|| GitError::InvalidFormat("receive-pack ng status missing message".into()))?;
6205    validate_protocol_v2_token("receive-pack status ref name", name)?;
6206    validate_receive_pack_status_message("receive-pack ng status message", message)?;
6207    Ok(ReceivePackCommandStatus::Ng {
6208        name: name.to_string(),
6209        message: message.to_string(),
6210    })
6211}
6212
6213fn format_receive_pack_command_status(status: &ReceivePackCommandStatus) -> Result<String> {
6214    match status {
6215        ReceivePackCommandStatus::Ok { name } => {
6216            validate_protocol_v2_token("receive-pack status ref name", name)?;
6217            Ok(format!("ok {name}"))
6218        }
6219        ReceivePackCommandStatus::Ng { name, message } => {
6220            validate_protocol_v2_token("receive-pack status ref name", name)?;
6221            validate_receive_pack_status_message("receive-pack ng status message", message)?;
6222            Ok(format!("ng {name} {message}"))
6223        }
6224    }
6225}
6226
6227fn parse_receive_pack_command_status_v2(text: &str) -> Result<ReceivePackCommandStatusV2> {
6228    if let Some(name) = text.strip_prefix("ok ") {
6229        validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6230        return Ok(ReceivePackCommandStatusV2::Ok {
6231            name: name.to_string(),
6232            options: ReceivePackCommandStatusV2Options::default(),
6233        });
6234    }
6235    let Some(rest) = text.strip_prefix("ng ") else {
6236        return Err(GitError::InvalidFormat(format!(
6237            "unsupported receive-pack report-status-v2 line {text}"
6238        )));
6239    };
6240    let (name, message) = rest.split_once(' ').ok_or_else(|| {
6241        GitError::InvalidFormat("receive-pack status-v2 ng status missing message".into())
6242    })?;
6243    validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6244    validate_receive_pack_status_message("receive-pack status-v2 ng message", message)?;
6245    Ok(ReceivePackCommandStatusV2::Ng {
6246        name: name.to_string(),
6247        message: message.to_string(),
6248    })
6249}
6250
6251fn format_receive_pack_command_status_v2(status: &ReceivePackCommandStatusV2) -> Result<String> {
6252    match status {
6253        ReceivePackCommandStatusV2::Ok { name, .. } => {
6254            validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6255            Ok(format!("ok {name}"))
6256        }
6257        ReceivePackCommandStatusV2::Ng { name, message } => {
6258            validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6259            validate_receive_pack_status_message("receive-pack status-v2 ng message", message)?;
6260            Ok(format!("ng {name} {message}"))
6261        }
6262    }
6263}
6264
6265fn parse_receive_pack_report_status_v2_option(
6266    format: ObjectFormat,
6267    text: &str,
6268    options: &mut ReceivePackCommandStatusV2Options,
6269) -> Result<()> {
6270    if let Some(refname) = text.strip_prefix("option refname ") {
6271        if options.refname.is_some() {
6272            return Err(GitError::InvalidFormat(
6273                "receive-pack report-status-v2 has duplicate refname option".into(),
6274            ));
6275        }
6276        validate_protocol_v2_token("receive-pack status-v2 option refname", refname)?;
6277        options.refname = Some(refname.to_string());
6278    } else if let Some(old_oid) = text.strip_prefix("option old-oid ") {
6279        if options.old_oid.is_some() {
6280            return Err(GitError::InvalidFormat(
6281                "receive-pack report-status-v2 has duplicate old-oid option".into(),
6282            ));
6283        }
6284        validate_protocol_v2_token("receive-pack status-v2 option old-oid", old_oid)?;
6285        options.old_oid = Some(ObjectId::from_hex(format, old_oid)?);
6286    } else if let Some(new_oid) = text.strip_prefix("option new-oid ") {
6287        if options.new_oid.is_some() {
6288            return Err(GitError::InvalidFormat(
6289                "receive-pack report-status-v2 has duplicate new-oid option".into(),
6290            ));
6291        }
6292        validate_protocol_v2_token("receive-pack status-v2 option new-oid", new_oid)?;
6293        options.new_oid = Some(ObjectId::from_hex(format, new_oid)?);
6294    } else if text == "option forced-update" {
6295        if options.forced_update {
6296            return Err(GitError::InvalidFormat(
6297                "receive-pack report-status-v2 has duplicate forced-update option".into(),
6298            ));
6299        }
6300        options.forced_update = true;
6301    } else {
6302        return Err(GitError::InvalidFormat(format!(
6303            "unsupported receive-pack report-status-v2 option {text}"
6304        )));
6305    }
6306    Ok(())
6307}
6308
6309fn format_receive_pack_report_status_v2_options(
6310    options: &ReceivePackCommandStatusV2Options,
6311) -> Result<Vec<String>> {
6312    let mut out = Vec::new();
6313    if let Some(refname) = &options.refname {
6314        validate_protocol_v2_token("receive-pack status-v2 option refname", refname)?;
6315        out.push(format!("option refname {refname}"));
6316    }
6317    if let Some(old_oid) = &options.old_oid {
6318        out.push(format!("option old-oid {old_oid}"));
6319    }
6320    if let Some(new_oid) = &options.new_oid {
6321        out.push(format!("option new-oid {new_oid}"));
6322    }
6323    if options.forced_update {
6324        out.push("option forced-update".into());
6325    }
6326    Ok(out)
6327}
6328
6329fn validate_receive_pack_status_message(label: &str, message: &str) -> Result<()> {
6330    if message.is_empty() {
6331        return Err(GitError::InvalidFormat(format!("{label} is empty")));
6332    }
6333    if message
6334        .bytes()
6335        .any(|byte| matches!(byte, b'\n' | b'\r' | 0))
6336    {
6337        return Err(GitError::InvalidFormat(format!(
6338            "{label} contains a delimiter byte"
6339        )));
6340    }
6341    Ok(())
6342}
6343
6344fn validate_receive_pack_push_option(option: &[u8]) -> Result<()> {
6345    if option.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
6346        return Err(GitError::InvalidFormat(
6347            "receive-pack push-option contains a delimiter byte".into(),
6348        ));
6349    }
6350    Ok(())
6351}
6352
6353fn validate_protocol_error_message(message: &str) -> Result<()> {
6354    if message.is_empty() {
6355        return Err(GitError::InvalidFormat(
6356            "protocol error message is empty".into(),
6357        ));
6358    }
6359    if message
6360        .bytes()
6361        .any(|byte| matches!(byte, b'\n' | b'\r' | 0))
6362    {
6363        return Err(GitError::InvalidFormat(
6364            "protocol error message contains a delimiter byte".into(),
6365        ));
6366    }
6367    Ok(())
6368}
6369
6370fn parse_capability_token(token: &str) -> Result<Capability> {
6371    if token.is_empty() {
6372        return Err(GitError::InvalidFormat("empty capability token".into()));
6373    }
6374    let (name, value) = token
6375        .split_once('=')
6376        .map_or((token, None), |(name, value)| (name, Some(value)));
6377    validate_capability_field("capability name", name)?;
6378    if let Some(value) = value {
6379        validate_capability_field("capability value", value)?;
6380    }
6381    Ok(Capability {
6382        name: name.to_string(),
6383        value: value.map(str::to_string),
6384    })
6385}
6386
6387fn parse_protocol_v2_capability_line(payload: &[u8]) -> Result<Capability> {
6388    let payload = trim_trailing_lf(payload);
6389    if payload.is_empty() {
6390        return Err(GitError::InvalidFormat(
6391            "empty protocol v2 capability line".into(),
6392        ));
6393    }
6394    let text =
6395        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6396    let (name, value) = text
6397        .split_once('=')
6398        .map_or((text, None), |(name, value)| (name, Some(value)));
6399    validate_capability_name(name)?;
6400    if let Some(value) = value {
6401        validate_protocol_v2_capability_value(value)?;
6402    }
6403    Ok(Capability {
6404        name: name.to_string(),
6405        value: value.map(str::to_string),
6406    })
6407}
6408
6409fn parse_protocol_v2_command_line(payload: &[u8]) -> Result<String> {
6410    let payload = trim_trailing_lf(payload);
6411    let text =
6412        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6413    let Some(command) = text.strip_prefix("command=") else {
6414        return Err(GitError::InvalidFormat(
6415            "protocol v2 command request missing command prefix".into(),
6416        ));
6417    };
6418    validate_capability_name(command)?;
6419    Ok(command.to_string())
6420}
6421
6422fn parse_protocol_v2_ls_refs_line(
6423    format: ObjectFormat,
6424    payload: &[u8],
6425) -> Result<ProtocolV2LsRefsRecord> {
6426    let payload = trim_trailing_lf(payload);
6427    if payload.is_empty() {
6428        return Err(GitError::InvalidFormat(
6429            "ls-refs response line is empty".into(),
6430        ));
6431    }
6432    let text =
6433        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6434    let mut fields = text.split(' ');
6435    let first = fields
6436        .next()
6437        .ok_or_else(|| GitError::InvalidFormat("ls-refs response line is empty".into()))?;
6438    if first == "unborn" {
6439        let name = fields
6440            .next()
6441            .ok_or_else(|| GitError::InvalidFormat("ls-refs unborn line is missing name".into()))?;
6442        validate_protocol_v2_token("ls-refs ref name", name)?;
6443        let (symref_target, attributes) = parse_protocol_v2_ls_refs_attributes(format, fields)?;
6444        return Ok(ProtocolV2LsRefsRecord::Unborn {
6445            name: name.to_string(),
6446            symref_target,
6447            attributes,
6448        });
6449    }
6450
6451    let oid = ObjectId::from_hex(format, first)?;
6452    let name = fields
6453        .next()
6454        .ok_or_else(|| GitError::InvalidFormat("ls-refs ref line is missing name".into()))?;
6455    validate_protocol_v2_token("ls-refs ref name", name)?;
6456    let (peeled, symref_target, attributes) =
6457        parse_protocol_v2_ls_refs_ref_attributes(format, fields)?;
6458    Ok(ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
6459        oid,
6460        name: name.to_string(),
6461        peeled,
6462        symref_target,
6463        attributes,
6464    }))
6465}
6466
6467fn parse_protocol_v2_ls_refs_ref_attributes<'a>(
6468    format: ObjectFormat,
6469    fields: impl Iterator<Item = &'a str>,
6470) -> Result<(Option<ObjectId>, Option<String>, Vec<String>)> {
6471    let mut peeled = None;
6472    let (symref_target, attributes) =
6473        parse_protocol_v2_ls_refs_attributes_with(format, fields, |attr| {
6474            if let Some(value) = attr.strip_prefix("peeled:") {
6475                if peeled.is_some() {
6476                    return Err(GitError::InvalidFormat(
6477                        "ls-refs response has duplicate peeled attribute".into(),
6478                    ));
6479                }
6480                peeled = Some(ObjectId::from_hex(format, value)?);
6481                return Ok(true);
6482            }
6483            Ok(false)
6484        })?;
6485    Ok((peeled, symref_target, attributes))
6486}
6487
6488fn parse_protocol_v2_ls_refs_attributes<'a>(
6489    format: ObjectFormat,
6490    fields: impl Iterator<Item = &'a str>,
6491) -> Result<(Option<String>, Vec<String>)> {
6492    parse_protocol_v2_ls_refs_attributes_with(format, fields, |attr| {
6493        if attr.starts_with("peeled:") {
6494            return Err(GitError::InvalidFormat(
6495                "ls-refs unborn line has peeled attribute".into(),
6496            ));
6497        }
6498        Ok(false)
6499    })
6500}
6501
6502fn parse_protocol_v2_ls_refs_attributes_with<'a, F>(
6503    _format: ObjectFormat,
6504    fields: impl Iterator<Item = &'a str>,
6505    mut handle_known: F,
6506) -> Result<(Option<String>, Vec<String>)>
6507where
6508    F: FnMut(&str) -> Result<bool>,
6509{
6510    let mut symref_target = None;
6511    let mut attributes = Vec::new();
6512    for attr in fields {
6513        validate_protocol_v2_token("ls-refs attribute", attr)?;
6514        if let Some(value) = attr.strip_prefix("symref-target:") {
6515            if symref_target.is_some() {
6516                return Err(GitError::InvalidFormat(
6517                    "ls-refs response has duplicate symref-target attribute".into(),
6518                ));
6519            }
6520            validate_protocol_v2_token("ls-refs symref-target", value)?;
6521            symref_target = Some(value.to_string());
6522        } else if !handle_known(attr)? {
6523            attributes.push(attr.to_string());
6524        }
6525    }
6526    Ok((symref_target, attributes))
6527}
6528
6529fn format_protocol_v2_ls_refs_record(record: &ProtocolV2LsRefsRecord) -> Result<String> {
6530    let mut out = String::new();
6531    match record {
6532        ProtocolV2LsRefsRecord::Ref(reference) => {
6533            validate_protocol_v2_token("ls-refs ref name", &reference.name)?;
6534            out.push_str(&reference.oid.to_string());
6535            out.push(' ');
6536            out.push_str(&reference.name);
6537            if let Some(peeled) = &reference.peeled {
6538                if peeled.format() != reference.oid.format() {
6539                    return Err(GitError::InvalidObjectId(
6540                        "ls-refs peeled object format does not match ref object format".into(),
6541                    ));
6542                }
6543                out.push(' ');
6544                out.push_str("peeled:");
6545                out.push_str(&peeled.to_string());
6546            }
6547            if let Some(target) = &reference.symref_target {
6548                validate_protocol_v2_token("ls-refs symref-target", target)?;
6549                out.push(' ');
6550                out.push_str("symref-target:");
6551                out.push_str(target);
6552            }
6553            append_protocol_v2_ls_refs_attributes(&mut out, &reference.attributes)?;
6554        }
6555        ProtocolV2LsRefsRecord::Unborn {
6556            name,
6557            symref_target,
6558            attributes,
6559        } => {
6560            validate_protocol_v2_token("ls-refs ref name", name)?;
6561            out.push_str("unborn ");
6562            out.push_str(name);
6563            if let Some(target) = symref_target {
6564                validate_protocol_v2_token("ls-refs symref-target", target)?;
6565                out.push(' ');
6566                out.push_str("symref-target:");
6567                out.push_str(target);
6568            }
6569            append_protocol_v2_ls_refs_attributes(&mut out, attributes)?;
6570        }
6571    }
6572    Ok(out)
6573}
6574
6575fn append_protocol_v2_ls_refs_attributes(out: &mut String, attributes: &[String]) -> Result<()> {
6576    for attr in attributes {
6577        validate_protocol_v2_token("ls-refs attribute", attr)?;
6578        if attr.starts_with("peeled:") || attr.starts_with("symref-target:") {
6579            return Err(GitError::InvalidFormat(
6580                "ls-refs generic attributes must not duplicate known attributes".into(),
6581            ));
6582        }
6583        out.push(' ');
6584        out.push_str(attr);
6585    }
6586    Ok(())
6587}
6588
6589fn parse_fetch_section_header(payload: &[u8]) -> Result<String> {
6590    let name = parse_protocol_v2_line_text("fetch response section", payload)?;
6591    validate_capability_name(name)?;
6592    Ok(name.to_string())
6593}
6594
6595fn flush_terminates_protocol_v2_response(frames: &[PktLineFrame], idx: usize) -> bool {
6596    idx + 1 == frames.len()
6597        || (idx + 2 == frames.len() && matches!(frames[idx + 1], PktLineFrame::ResponseEnd))
6598}
6599
6600fn parse_fetch_section(
6601    format: ObjectFormat,
6602    name: String,
6603    lines: Vec<Vec<u8>>,
6604) -> Result<ProtocolV2FetchResponseSection> {
6605    match name.as_str() {
6606        "acknowledgments" => lines
6607            .iter()
6608            .map(|line| parse_fetch_acknowledgment(format, line))
6609            .collect::<Result<Vec<_>>>()
6610            .map(ProtocolV2FetchResponseSection::Acknowledgments),
6611        "shallow-info" => lines
6612            .iter()
6613            .map(|line| parse_fetch_shallow_info(format, line))
6614            .collect::<Result<Vec<_>>>()
6615            .map(ProtocolV2FetchResponseSection::ShallowInfo),
6616        "wanted-refs" => lines
6617            .iter()
6618            .map(|line| parse_fetch_wanted_ref(format, line))
6619            .collect::<Result<Vec<_>>>()
6620            .map(ProtocolV2FetchResponseSection::WantedRefs),
6621        "packfile-uris" => lines
6622            .iter()
6623            .map(|line| parse_fetch_packfile_uri(format, line))
6624            .collect::<Result<Vec<_>>>()
6625            .map(ProtocolV2FetchResponseSection::PackfileUris),
6626        "packfile" => Ok(ProtocolV2FetchResponseSection::Packfile(lines)),
6627        _ => Ok(ProtocolV2FetchResponseSection::Unknown { name, lines }),
6628    }
6629}
6630
6631fn parse_fetch_acknowledgment(
6632    format: ObjectFormat,
6633    line: &[u8],
6634) -> Result<ProtocolV2FetchAcknowledgment> {
6635    let text = parse_protocol_v2_line_text("fetch acknowledgment", line)?;
6636    match text {
6637        "NAK" => Ok(ProtocolV2FetchAcknowledgment::Nak),
6638        "ready" => Ok(ProtocolV2FetchAcknowledgment::Ready),
6639        value if value.starts_with("ACK ") => Ok(ProtocolV2FetchAcknowledgment::Ack(
6640            parse_oid_argument(format, "fetch ACK", value, "ACK ")?,
6641        )),
6642        other => Err(GitError::InvalidFormat(format!(
6643            "unsupported fetch acknowledgment {other}"
6644        ))),
6645    }
6646}
6647
6648fn parse_fetch_shallow_info(
6649    format: ObjectFormat,
6650    line: &[u8],
6651) -> Result<ProtocolV2FetchShallowInfo> {
6652    let text = parse_protocol_v2_line_text("fetch shallow-info", line)?;
6653    if text.starts_with("shallow ") {
6654        return Ok(ProtocolV2FetchShallowInfo::Shallow(parse_oid_argument(
6655            format,
6656            "fetch shallow",
6657            text,
6658            "shallow ",
6659        )?));
6660    }
6661    if text.starts_with("unshallow ") {
6662        return Ok(ProtocolV2FetchShallowInfo::Unshallow(parse_oid_argument(
6663            format,
6664            "fetch unshallow",
6665            text,
6666            "unshallow ",
6667        )?));
6668    }
6669    Err(GitError::InvalidFormat(format!(
6670        "unsupported fetch shallow-info {text}"
6671    )))
6672}
6673
6674fn parse_fetch_wanted_ref(format: ObjectFormat, line: &[u8]) -> Result<ProtocolV2FetchWantedRef> {
6675    let text = parse_protocol_v2_line_text("fetch wanted-ref", line)?;
6676    let (oid, name) = text
6677        .split_once(' ')
6678        .ok_or_else(|| GitError::InvalidFormat("fetch wanted-ref is missing name".into()))?;
6679    validate_protocol_v2_token("fetch wanted-ref name", name)?;
6680    Ok(ProtocolV2FetchWantedRef {
6681        oid: ObjectId::from_hex(format, oid)?,
6682        name: name.to_string(),
6683    })
6684}
6685
6686fn parse_fetch_packfile_uri(
6687    format: ObjectFormat,
6688    line: &[u8],
6689) -> Result<ProtocolV2FetchPackfileUri> {
6690    let text = parse_protocol_v2_line_text("fetch packfile-uri", line)?;
6691    let (pack_hash, uri) = text
6692        .split_once(' ')
6693        .ok_or_else(|| GitError::InvalidFormat("fetch packfile-uri is missing uri".into()))?;
6694    validate_protocol_v2_token("fetch packfile-uri hash", pack_hash)?;
6695    validate_protocol_v2_token("fetch packfile-uri", uri)?;
6696    Ok(ProtocolV2FetchPackfileUri {
6697        pack_hash: ObjectId::from_hex(format, pack_hash)?,
6698        uri: uri.to_string(),
6699    })
6700}
6701
6702fn protocol_v2_fetch_section_name(section: &ProtocolV2FetchResponseSection) -> &str {
6703    match section {
6704        ProtocolV2FetchResponseSection::Acknowledgments(_) => "acknowledgments",
6705        ProtocolV2FetchResponseSection::ShallowInfo(_) => "shallow-info",
6706        ProtocolV2FetchResponseSection::WantedRefs(_) => "wanted-refs",
6707        ProtocolV2FetchResponseSection::PackfileUris(_) => "packfile-uris",
6708        ProtocolV2FetchResponseSection::Packfile(_) => "packfile",
6709        ProtocolV2FetchResponseSection::Unknown { name, .. } => name,
6710    }
6711}
6712
6713fn format_protocol_v2_fetch_section_lines(
6714    section: &ProtocolV2FetchResponseSection,
6715) -> Result<Vec<Vec<u8>>> {
6716    match section {
6717        ProtocolV2FetchResponseSection::Acknowledgments(acks) => acks
6718            .iter()
6719            .map(|ack| match ack {
6720                ProtocolV2FetchAcknowledgment::Nak => Ok(line_from_str("NAK")),
6721                ProtocolV2FetchAcknowledgment::Ack(oid) => Ok(line_from_str(&format!("ACK {oid}"))),
6722                ProtocolV2FetchAcknowledgment::Ready => Ok(line_from_str("ready")),
6723            })
6724            .collect(),
6725        ProtocolV2FetchResponseSection::ShallowInfo(entries) => entries
6726            .iter()
6727            .map(|entry| match entry {
6728                ProtocolV2FetchShallowInfo::Shallow(oid) => {
6729                    Ok(line_from_str(&format!("shallow {oid}")))
6730                }
6731                ProtocolV2FetchShallowInfo::Unshallow(oid) => {
6732                    Ok(line_from_str(&format!("unshallow {oid}")))
6733                }
6734            })
6735            .collect(),
6736        ProtocolV2FetchResponseSection::WantedRefs(refs) => refs
6737            .iter()
6738            .map(|wanted| {
6739                validate_protocol_v2_token("fetch wanted-ref name", &wanted.name)?;
6740                Ok(line_from_str(&format!("{} {}", wanted.oid, wanted.name)))
6741            })
6742            .collect(),
6743        ProtocolV2FetchResponseSection::PackfileUris(uris) => uris
6744            .iter()
6745            .map(|packfile_uri| {
6746                validate_protocol_v2_token("fetch packfile-uri", &packfile_uri.uri)?;
6747                Ok(line_from_str(&format!(
6748                    "{} {}",
6749                    packfile_uri.pack_hash, packfile_uri.uri
6750                )))
6751            })
6752            .collect(),
6753        ProtocolV2FetchResponseSection::Packfile(lines) => Ok(lines.clone()),
6754        ProtocolV2FetchResponseSection::Unknown { name, lines } => {
6755            validate_capability_name(name)?;
6756            for line in lines {
6757                validate_protocol_v2_line("fetch unknown section line", line)?;
6758            }
6759            Ok(lines.clone())
6760        }
6761    }
6762}
6763
6764fn parse_protocol_v2_object_info_record(
6765    format: ObjectFormat,
6766    line: &[u8],
6767) -> Result<ProtocolV2ObjectInfoRecord> {
6768    let text = parse_protocol_v2_line_text("object-info record", line)?;
6769    let mut fields = text.split(' ');
6770    let oid = fields
6771        .next()
6772        .ok_or_else(|| GitError::InvalidFormat("object-info record is missing oid".into()))?;
6773    let size = fields
6774        .next()
6775        .ok_or_else(|| GitError::InvalidFormat("object-info record is missing size".into()))?;
6776    if fields.next().is_some() {
6777        return Err(GitError::InvalidFormat(
6778            "object-info record has too many fields".into(),
6779        ));
6780    }
6781    validate_protocol_v2_token("object-info oid", oid)?;
6782    validate_protocol_v2_token("object-info size", size)?;
6783    let size = size
6784        .parse::<u64>()
6785        .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6786    Ok(ProtocolV2ObjectInfoRecord {
6787        oid: ObjectId::from_hex(format, oid)?,
6788        size,
6789    })
6790}
6791
6792fn parse_dumb_http_info_ref_record(format: ObjectFormat, line: &[u8]) -> Result<DumbHttpRefRecord> {
6793    validate_dumb_http_info_ref_line(line)?;
6794    let line = trim_trailing_lf(line);
6795    let tab = line
6796        .iter()
6797        .position(|byte| *byte == b'\t')
6798        .ok_or_else(|| GitError::InvalidFormat("dumb HTTP ref record is missing name".into()))?;
6799    let (oid, name) = (&line[..tab], &line[tab + 1..]);
6800    let oid = std::str::from_utf8(oid).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6801    validate_protocol_v2_token("dumb HTTP ref oid", oid)?;
6802    let name = std::str::from_utf8(name).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6803    let (name, peeled) = name
6804        .strip_suffix("^{}")
6805        .map_or((name, false), |name| (name, true));
6806    validate_dumb_http_ref_name(name)?;
6807    Ok(DumbHttpRefRecord {
6808        oid: ObjectId::from_hex(format, oid)?,
6809        name: name.to_string(),
6810        peeled,
6811    })
6812}
6813
6814fn parse_dumb_http_alternate(line: &[u8]) -> Result<String> {
6815    validate_dumb_http_alternate_line(line)?;
6816    let line = trim_trailing_lf(line);
6817    let alternate =
6818        std::str::from_utf8(line).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6819    validate_dumb_http_alternate(alternate)?;
6820    Ok(alternate.to_string())
6821}
6822
6823fn parse_dumb_http_pack_record(format: ObjectFormat, line: &[u8]) -> Result<DumbHttpPackRecord> {
6824    validate_dumb_http_info_ref_line(line)?;
6825    let line = parse_protocol_v2_line_text("dumb HTTP pack record", line)?;
6826    let pack_name = line
6827        .strip_prefix("P ")
6828        .ok_or_else(|| GitError::InvalidFormat("dumb HTTP pack record must start with P".into()))?;
6829    let hash = pack_name
6830        .strip_prefix("pack-")
6831        .and_then(|value| value.strip_suffix(".pack"))
6832        .ok_or_else(|| GitError::InvalidFormat("invalid dumb HTTP pack name".into()))?;
6833    validate_protocol_v2_token("dumb HTTP pack hash", hash)?;
6834    Ok(DumbHttpPackRecord {
6835        hash: ObjectId::from_hex(format, hash)?,
6836    })
6837}
6838
6839fn encode_protocol_v2_capability(capability: &Capability) -> Result<Vec<u8>> {
6840    validate_capability_name(&capability.name)?;
6841    let mut out = capability.name.as_bytes().to_vec();
6842    if let Some(value) = &capability.value {
6843        validate_protocol_v2_capability_value(value)?;
6844        out.push(b'=');
6845        out.extend_from_slice(value.as_bytes());
6846    }
6847    Ok(out)
6848}
6849
6850fn validate_capability_field(label: &str, value: &str) -> Result<()> {
6851    if value.is_empty() {
6852        return Err(GitError::InvalidFormat(format!("{label} is empty")));
6853    }
6854    if value
6855        .bytes()
6856        .any(|byte| matches!(byte, b' ' | b'\n' | b'\r' | b'\t' | 0))
6857    {
6858        return Err(GitError::InvalidFormat(format!(
6859            "{label} contains a delimiter byte"
6860        )));
6861    }
6862    Ok(())
6863}
6864
6865fn validate_capability_name(value: &str) -> Result<()> {
6866    validate_capability_field("capability name", value)?;
6867    if value.bytes().any(|byte| byte == b'=') {
6868        return Err(GitError::InvalidFormat(
6869            "capability name contains a delimiter byte".into(),
6870        ));
6871    }
6872    Ok(())
6873}
6874
6875fn validate_protocol_v2_capability_value(value: &str) -> Result<()> {
6876    if value.is_empty() {
6877        return Err(GitError::InvalidFormat(
6878            "protocol v2 capability value is empty".into(),
6879        ));
6880    }
6881    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6882        return Err(GitError::InvalidFormat(
6883            "protocol v2 capability value contains a delimiter byte".into(),
6884        ));
6885    }
6886    Ok(())
6887}
6888
6889fn validate_protocol_v2_argument(value: &[u8]) -> Result<()> {
6890    if value.is_empty() {
6891        return Err(GitError::InvalidFormat(
6892            "protocol v2 command argument is empty".into(),
6893        ));
6894    }
6895    if value.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
6896        return Err(GitError::InvalidFormat(
6897            "protocol v2 command argument contains a delimiter byte".into(),
6898        ));
6899    }
6900    Ok(())
6901}
6902
6903fn validate_upload_archive_argument(value: &str) -> Result<()> {
6904    if value.is_empty() {
6905        return Err(GitError::InvalidFormat(
6906            "upload-archive argument is empty".into(),
6907        ));
6908    }
6909    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6910        return Err(GitError::InvalidFormat(
6911            "upload-archive argument contains a delimiter byte".into(),
6912        ));
6913    }
6914    Ok(())
6915}
6916
6917fn validate_upload_archive_status_message(value: &str) -> Result<()> {
6918    if value.is_empty() {
6919        return Err(GitError::InvalidFormat(
6920            "upload-archive status message is empty".into(),
6921        ));
6922    }
6923    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6924        return Err(GitError::InvalidFormat(
6925            "upload-archive status message contains a delimiter byte".into(),
6926        ));
6927    }
6928    Ok(())
6929}
6930
6931fn non_empty(value: &str) -> Option<&str> {
6932    (!value.is_empty()).then_some(value)
6933}
6934
6935fn validate_refspec_value(value: &str) -> Result<()> {
6936    if value.is_empty() {
6937        return Err(GitError::InvalidFormat("refspec is empty".into()));
6938    }
6939    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6940        return Err(GitError::InvalidFormat(
6941            "refspec contains a delimiter byte".into(),
6942        ));
6943    }
6944    Ok(())
6945}
6946
6947fn validate_refspec_endpoint(label: &str, value: &str) -> Result<()> {
6948    if value.is_empty() {
6949        return Err(GitError::InvalidFormat(format!("{label} is empty")));
6950    }
6951    if value
6952        .bytes()
6953        .any(|byte| matches!(byte, b':' | b' ' | b'\t' | b'\n' | b'\r' | 0))
6954    {
6955        return Err(GitError::InvalidFormat(format!(
6956            "{label} contains a delimiter byte"
6957        )));
6958    }
6959    Ok(())
6960}
6961
6962fn count_refspec_wildcards(value: &str) -> usize {
6963    value.bytes().filter(|byte| *byte == b'*').count()
6964}
6965
6966fn validate_refspec_shape(refspec: &RefSpec) -> Result<()> {
6967    if refspec.force && refspec.negative {
6968        return Err(GitError::InvalidFormat(
6969            "negative refspec must not be forced".into(),
6970        ));
6971    }
6972    if refspec.negative && refspec.dst.is_some() {
6973        return Err(GitError::InvalidFormat(
6974            "negative refspec must not have a destination".into(),
6975        ));
6976    }
6977    if refspec.negative && refspec.src.is_none() {
6978        return Err(GitError::InvalidFormat(
6979            "negative refspec is missing a source".into(),
6980        ));
6981    }
6982    if refspec.src.is_none() && refspec.dst.is_none() && refspec.negative {
6983        return Err(GitError::InvalidFormat(
6984            "refspec must include a source or destination".into(),
6985        ));
6986    }
6987    if let Some(src) = &refspec.src {
6988        validate_refspec_endpoint("refspec source", src)?;
6989    }
6990    if let Some(dst) = &refspec.dst {
6991        validate_refspec_endpoint("refspec destination", dst)?;
6992    }
6993    let src_pattern_count = refspec
6994        .src
6995        .as_deref()
6996        .map(count_refspec_wildcards)
6997        .unwrap_or(0);
6998    let dst_pattern_count = refspec
6999        .dst
7000        .as_deref()
7001        .map(count_refspec_wildcards)
7002        .unwrap_or(0);
7003    if src_pattern_count > 1 || dst_pattern_count > 1 {
7004        return Err(GitError::InvalidFormat(
7005            "refspec endpoint has too many wildcards".into(),
7006        ));
7007    }
7008    if refspec.dst.is_some() && (src_pattern_count == 1) != (dst_pattern_count == 1) {
7009        return Err(GitError::InvalidFormat(
7010            "refspec wildcard must appear in both source and destination".into(),
7011        ));
7012    }
7013    if refspec.pattern != (src_pattern_count == 1 || dst_pattern_count == 1) {
7014        return Err(GitError::InvalidFormat(
7015            "refspec pattern flag does not match endpoints".into(),
7016        ));
7017    }
7018    Ok(())
7019}
7020
7021fn parse_fetch_head_record(format: ObjectFormat, line: &[u8]) -> Result<FetchHeadRecord> {
7022    validate_fetch_head_line(line)?;
7023    let line = trim_trailing_lf(line);
7024    let mut fields = line.splitn(3, |byte| *byte == b'\t');
7025    let oid = fields
7026        .next()
7027        .ok_or_else(|| GitError::InvalidFormat("FETCH_HEAD record is missing oid".into()))?;
7028    let merge_marker = fields.next().ok_or_else(|| {
7029        GitError::InvalidFormat("FETCH_HEAD record is missing merge marker".into())
7030    })?;
7031    let description = fields.next().ok_or_else(|| {
7032        GitError::InvalidFormat("FETCH_HEAD record is missing description".into())
7033    })?;
7034    let oid = std::str::from_utf8(oid).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
7035    validate_protocol_v2_token("FETCH_HEAD oid", oid)?;
7036    let not_for_merge = match merge_marker {
7037        b"" => false,
7038        b"not-for-merge" => true,
7039        _ => {
7040            return Err(GitError::InvalidFormat(
7041                "FETCH_HEAD record has invalid merge marker".into(),
7042            ));
7043        }
7044    };
7045    let description =
7046        std::str::from_utf8(description).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
7047    validate_fetch_head_description_field(description)?;
7048    Ok(FetchHeadRecord {
7049        oid: ObjectId::from_hex(format, oid)?,
7050        not_for_merge,
7051        description: description.to_string(),
7052    })
7053}
7054
7055fn validate_fetch_head_line(value: &[u8]) -> Result<()> {
7056    if value.is_empty() {
7057        return Err(GitError::InvalidFormat("FETCH_HEAD record is empty".into()));
7058    }
7059    if !value.ends_with(b"\n") {
7060        return Err(GitError::InvalidFormat(
7061            "FETCH_HEAD record missing LF".into(),
7062        ));
7063    }
7064    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7065        return Err(GitError::InvalidFormat(
7066            "FETCH_HEAD record contains a delimiter byte".into(),
7067        ));
7068    }
7069    Ok(())
7070}
7071
7072fn validate_fetch_head_description_field(value: &str) -> Result<()> {
7073    if value.is_empty() {
7074        return Err(GitError::InvalidFormat(
7075            "FETCH_HEAD description is empty".into(),
7076        ));
7077    }
7078    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
7079        return Err(GitError::InvalidFormat(
7080            "FETCH_HEAD description contains a delimiter byte".into(),
7081        ));
7082    }
7083    Ok(())
7084}
7085
7086fn refspec_is_excluded(negative: &[&RefSpec], source: &str) -> Result<bool> {
7087    for refspec in negative {
7088        if refspec_matches_source(refspec, source)? {
7089            return Ok(true);
7090        }
7091    }
7092    Ok(false)
7093}
7094
7095fn zero_object_id(format: ObjectFormat) -> Result<ObjectId> {
7096    Ok(ObjectId::null(format))
7097}
7098
7099fn local_ref<'a>(refs: &'a [PushSourceRef], name: &str) -> Option<&'a PushSourceRef> {
7100    refs.iter().find(|reference| reference.name == name)
7101}
7102
7103fn remote_ref<'a>(refs: &'a [RefAdvertisement], name: &str) -> Option<&'a RefAdvertisement> {
7104    refs.iter().find(|reference| reference.name == name)
7105}
7106
7107fn validate_push_source_ref(format: ObjectFormat, reference: &PushSourceRef) -> Result<()> {
7108    if reference.oid.format() != format {
7109        return Err(GitError::InvalidObjectId(
7110            "push source ref object format does not match repository".into(),
7111        ));
7112    }
7113    validate_refspec_endpoint("push source ref name", &reference.name)
7114}
7115
7116fn require_receive_pack_feature(advertised: bool, name: &str) -> Result<()> {
7117    if advertised {
7118        Ok(())
7119    } else {
7120        Err(GitError::InvalidFormat(format!(
7121            "receive-pack feature {name} was not advertised"
7122        )))
7123    }
7124}
7125
7126fn validate_smart_http_service(service: GitService) -> Result<()> {
7127    match service {
7128        GitService::UploadPack | GitService::ReceivePack => Ok(()),
7129        GitService::UploadArchive => Err(GitError::InvalidFormat(
7130            "smart HTTP only supports upload-pack and receive-pack services".into(),
7131        )),
7132    }
7133}
7134
7135fn normalize_http_repository_path(path: &str) -> Result<String> {
7136    if path.is_empty() {
7137        return Err(GitError::InvalidFormat(
7138            "smart HTTP repository path is empty".into(),
7139        ));
7140    }
7141    if !path.starts_with('/') {
7142        return Err(GitError::InvalidFormat(
7143            "smart HTTP repository path must start with /".into(),
7144        ));
7145    }
7146    if path
7147        .bytes()
7148        .any(|byte| matches!(byte, b'\n' | b'\r' | 0 | b'?' | b'#'))
7149    {
7150        return Err(GitError::InvalidFormat(
7151            "smart HTTP repository path contains a delimiter byte".into(),
7152        ));
7153    }
7154    let normalized = path.trim_end_matches('/');
7155    Ok(if normalized.is_empty() {
7156        "/".into()
7157    } else {
7158        normalized.to_string()
7159    })
7160}
7161
7162fn dumb_http_pack_resource_path(
7163    repository_path: &str,
7164    hash: &ObjectId,
7165    suffix: &str,
7166) -> Result<String> {
7167    let repository_path = normalize_http_repository_path(repository_path)?;
7168    Ok(format!(
7169        "{repository_path}/objects/pack/pack-{hash}.{suffix}"
7170    ))
7171}
7172
7173fn parse_smart_http_content_type(value: &str, suffix: &str) -> Result<GitService> {
7174    let value = value.trim();
7175    if value.is_empty() {
7176        return Err(GitError::InvalidFormat(
7177            "smart HTTP content type is empty".into(),
7178        ));
7179    }
7180    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
7181        return Err(GitError::InvalidFormat(
7182            "smart HTTP content type contains a delimiter byte".into(),
7183        ));
7184    }
7185    let value = value.to_ascii_lowercase();
7186    let service = value
7187        .strip_prefix("application/x-")
7188        .and_then(|value| value.strip_suffix(suffix))
7189        .ok_or_else(|| GitError::InvalidFormat("invalid smart HTTP content type".into()))?;
7190    let service = parse_git_service(service)?;
7191    validate_smart_http_service(service)?;
7192    Ok(service)
7193}
7194
7195fn validate_dumb_http_info_ref_line(value: &[u8]) -> Result<()> {
7196    if value.is_empty() {
7197        return Err(GitError::InvalidFormat(
7198            "dumb HTTP ref record is empty".into(),
7199        ));
7200    }
7201    if !value.ends_with(b"\n") {
7202        return Err(GitError::InvalidFormat(
7203            "dumb HTTP ref record missing LF".into(),
7204        ));
7205    }
7206    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7207        return Err(GitError::InvalidFormat(
7208            "dumb HTTP ref record contains a delimiter byte".into(),
7209        ));
7210    }
7211    Ok(())
7212}
7213
7214fn validate_dumb_http_ref_name(value: &str) -> Result<()> {
7215    validate_protocol_v2_token("dumb HTTP ref name", value)?;
7216    if value.ends_with("^{}") {
7217        return Err(GitError::InvalidFormat(
7218            "dumb HTTP ref name must not include peeled suffix".into(),
7219        ));
7220    }
7221    Ok(())
7222}
7223
7224fn validate_dumb_http_alternate_line(value: &[u8]) -> Result<()> {
7225    if value.is_empty() {
7226        return Err(GitError::InvalidFormat(
7227            "dumb HTTP alternate is empty".into(),
7228        ));
7229    }
7230    if !value.ends_with(b"\n") {
7231        return Err(GitError::InvalidFormat(
7232            "dumb HTTP alternate missing LF".into(),
7233        ));
7234    }
7235    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7236        return Err(GitError::InvalidFormat(
7237            "dumb HTTP alternate contains a delimiter byte".into(),
7238        ));
7239    }
7240    Ok(())
7241}
7242
7243fn validate_dumb_http_alternate(value: &str) -> Result<()> {
7244    if value.is_empty() {
7245        return Err(GitError::InvalidFormat(
7246            "dumb HTTP alternate is empty".into(),
7247        ));
7248    }
7249    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
7250        return Err(GitError::InvalidFormat(
7251            "dumb HTTP alternate contains a delimiter byte".into(),
7252        ));
7253    }
7254    Ok(())
7255}
7256
7257fn validate_protocol_v2_token(label: &str, value: &str) -> Result<()> {
7258    if value.is_empty() {
7259        return Err(GitError::InvalidFormat(format!("{label} is empty")));
7260    }
7261    if value
7262        .bytes()
7263        .any(|byte| matches!(byte, b' ' | b'\n' | b'\r' | 0))
7264    {
7265        return Err(GitError::InvalidFormat(format!(
7266            "{label} contains a delimiter byte"
7267        )));
7268    }
7269    Ok(())
7270}
7271
7272fn validate_protocol_v2_line(label: &str, value: &[u8]) -> Result<()> {
7273    if value.is_empty() {
7274        return Err(GitError::InvalidFormat(format!("{label} is empty")));
7275    }
7276    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7277        return Err(GitError::InvalidFormat(format!(
7278            "{label} contains a delimiter byte"
7279        )));
7280    }
7281    Ok(())
7282}
7283
7284fn parse_protocol_v2_line_text<'a>(label: &str, value: &'a [u8]) -> Result<&'a str> {
7285    validate_protocol_v2_line(label, value)?;
7286    let value = trim_trailing_lf(value);
7287    if value.is_empty() {
7288        return Err(GitError::InvalidFormat(format!("{label} is empty")));
7289    }
7290    if value.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
7291        return Err(GitError::InvalidFormat(format!(
7292            "{label} contains a delimiter byte"
7293        )));
7294    }
7295    std::str::from_utf8(value).map_err(|err| GitError::InvalidFormat(err.to_string()))
7296}
7297
7298fn parse_oid_argument(
7299    format: ObjectFormat,
7300    label: &str,
7301    value: &str,
7302    prefix: &str,
7303) -> Result<ObjectId> {
7304    let oid = value
7305        .strip_prefix(prefix)
7306        .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
7307    validate_protocol_v2_token(label, oid)?;
7308    ObjectId::from_hex(format, oid)
7309}
7310
7311fn parse_u32_argument(label: &str, value: &str, prefix: &str) -> Result<u32> {
7312    let number = value
7313        .strip_prefix(prefix)
7314        .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
7315    validate_protocol_v2_token(label, number)?;
7316    let parsed = number
7317        .parse::<u32>()
7318        .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
7319    if parsed == 0 {
7320        return Err(GitError::InvalidFormat(format!("{label} must be positive")));
7321    }
7322    Ok(parsed)
7323}
7324
7325fn parse_u64_argument(label: &str, value: &str, prefix: &str) -> Result<u64> {
7326    let number = value
7327        .strip_prefix(prefix)
7328        .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
7329    validate_protocol_v2_token(label, number)?;
7330    number
7331        .parse::<u64>()
7332        .map_err(|err| GitError::InvalidFormat(err.to_string()))
7333}
7334
7335fn line(mut payload: Vec<u8>) -> Vec<u8> {
7336    payload.push(b'\n');
7337    payload
7338}
7339
7340fn line_from_str(payload: &str) -> Vec<u8> {
7341    line(payload.as_bytes().to_vec())
7342}
7343
7344fn trim_trailing_lf(input: &[u8]) -> &[u8] {
7345    input.strip_suffix(b"\n").unwrap_or(input)
7346}
7347
7348#[cfg(test)]
7349mod tests {
7350    use super::*;
7351
7352    #[test]
7353    fn pkt_line_frame_encodes_data_and_control_frames() {
7354        assert_eq!(PktLine(b"hello\n".to_vec()).encode(), b"000ahello\n");
7355        assert_eq!(
7356            PktLineFrame::data(b"hello\n".to_vec())
7357                .expect("test operation should succeed")
7358                .encode(),
7359            b"000ahello\n"
7360        );
7361        assert_eq!(PktLineFrame::Flush.encode(), b"0000");
7362        assert_eq!(PktLineFrame::Delimiter.encode(), b"0001");
7363        assert_eq!(PktLineFrame::ResponseEnd.encode(), b"0002");
7364        assert_eq!(
7365            PktLineFrame::data(b"hello\n".to_vec())
7366                .expect("test operation should succeed")
7367                .try_encode()
7368                .expect("test operation should succeed"),
7369            b"000ahello\n"
7370        );
7371    }
7372
7373    #[test]
7374    fn pkt_line_frame_parses_data_and_control_frames() {
7375        assert_eq!(
7376            PktLineFrame::parse(b"000ahello\n").expect("test operation should succeed"),
7377            (PktLineFrame::Data(b"hello\n".to_vec()), 10)
7378        );
7379        assert_eq!(
7380            PktLineFrame::parse(b"0000").expect("test operation should succeed"),
7381            (PktLineFrame::Flush, 4)
7382        );
7383        assert_eq!(
7384            PktLineFrame::parse(b"0001").expect("test operation should succeed"),
7385            (PktLineFrame::Delimiter, 4)
7386        );
7387        assert_eq!(
7388            PktLineFrame::parse(b"0002").expect("test operation should succeed"),
7389            (PktLineFrame::ResponseEnd, 4)
7390        );
7391    }
7392
7393    #[test]
7394    fn pkt_line_stream_parses_multiple_frames() {
7395        let frames = parse_pkt_line_stream(b"000eversion 2\n00010009done\n0000")
7396            .expect("test operation should succeed");
7397        assert_eq!(
7398            frames,
7399            vec![
7400                PktLineFrame::Data(b"version 2\n".to_vec()),
7401                PktLineFrame::Delimiter,
7402                PktLineFrame::Data(b"done\n".to_vec()),
7403                PktLineFrame::Flush,
7404            ]
7405        );
7406    }
7407
7408    #[test]
7409    fn pkt_line_stream_reads_and_writes_incremental_io() {
7410        let frames = vec![
7411            PktLineFrame::Data(b"version 2\n".to_vec()),
7412            PktLineFrame::Delimiter,
7413            PktLineFrame::Data(b"done\n".to_vec()),
7414            PktLineFrame::Flush,
7415        ];
7416        let mut encoded = Vec::new();
7417        write_pkt_line_frames(&mut encoded, &frames).expect("test operation should succeed");
7418        assert_eq!(encoded, b"000eversion 2\n00010009done\n0000");
7419        assert_eq!(
7420            read_pkt_line_frames(&mut encoded.as_slice()).expect("test operation should succeed"),
7421            frames
7422        );
7423
7424        let mut empty: &[u8] = b"";
7425        assert_eq!(
7426            read_pkt_line_frame(&mut empty).expect("test operation should succeed"),
7427            None
7428        );
7429    }
7430
7431    #[test]
7432    fn pkt_line_stream_reads_until_control_packets() {
7433        let input = b"000eversion 2\n0000trailing";
7434        let frames = read_pkt_line_frames_until_flush(&mut &input[..])
7435            .expect("test operation should succeed");
7436        assert_eq!(
7437            frames,
7438            vec![
7439                PktLineFrame::Data(b"version 2\n".to_vec()),
7440                PktLineFrame::Flush,
7441            ]
7442        );
7443
7444        let input = b"0009done\n0002next";
7445        let frames = read_pkt_line_frames_until_response_end(&mut &input[..])
7446            .expect("test operation should succeed");
7447        assert_eq!(
7448            frames,
7449            vec![
7450                PktLineFrame::Data(b"done\n".to_vec()),
7451                PktLineFrame::ResponseEnd,
7452            ]
7453        );
7454        assert!(read_pkt_line_frames_until_flush(&mut &b"0009done\n"[..]).is_err());
7455    }
7456
7457    #[test]
7458    fn pkt_line_rejects_invalid_lengths() {
7459        assert!(PktLineFrame::parse(b"000").is_err());
7460        assert!(PktLineFrame::parse(b"0003").is_err());
7461        assert!(PktLineFrame::parse(b"000ahello").is_err());
7462        assert!(PktLineFrame::parse(b"zzzz").is_err());
7463        assert!(read_pkt_line_frame(&mut &b"000"[..]).is_err());
7464        assert!(read_pkt_line_frame(&mut &b"0003"[..]).is_err());
7465    }
7466
7467    #[test]
7468    fn pkt_line_rejects_oversized_data() {
7469        let payload = vec![b'x'; PKT_LINE_MAX_PAYLOAD_LEN + 1];
7470        assert!(PktLineFrame::data(payload.clone()).is_err());
7471        assert!(PktLine(payload.clone()).try_encode().is_err());
7472        assert!(PktLineFrame::Data(payload.clone()).try_encode().is_err());
7473        assert!(write_pkt_line_frame(&mut Vec::new(), &PktLineFrame::Data(payload)).is_err());
7474        assert!(PktLineFrame::parse(b"fff1").is_err());
7475    }
7476
7477    #[test]
7478    fn protocol_error_lines_parse_encode_and_stream() {
7479        let error = parse_error_line(b"ERR remote rejected request\n")
7480            .expect("test operation should succeed");
7481        assert_eq!(
7482            error,
7483            ProtocolErrorLine {
7484                message: "remote rejected request".into(),
7485            }
7486        );
7487        assert_eq!(
7488            encode_error_line(&error).expect("test operation should succeed"),
7489            b"ERR remote rejected request\n"
7490        );
7491        assert_eq!(
7492            parse_error_frame(&PktLineFrame::Data(
7493                b"ERR remote rejected request\n".to_vec()
7494            ))
7495            .expect("test operation should succeed"),
7496            Some(error.clone())
7497        );
7498        assert_eq!(
7499            parse_error_frame(&PktLineFrame::Data(b"NAK\n".to_vec()))
7500                .expect("test operation should succeed"),
7501            None
7502        );
7503
7504        let mut encoded = Vec::new();
7505        write_error_line(&mut encoded, &error).expect("test operation should succeed");
7506        encoded.extend_from_slice(b"tail");
7507        let mut input = encoded.as_slice();
7508        assert_eq!(
7509            read_error_line(&mut input).expect("test operation should succeed"),
7510            error
7511        );
7512        assert_eq!(input, b"tail");
7513    }
7514
7515    #[test]
7516    fn protocol_error_lines_reject_malformed_messages() {
7517        assert!(parse_error_line(b"ERR\n").is_err());
7518        assert!(parse_error_line(b"ERR \n").is_err());
7519        assert!(parse_error_line(b"ERR bad\0message\n").is_err());
7520        assert!(parse_error_line(b"NAK\n").is_err());
7521        assert!(
7522            encode_error_line(&ProtocolErrorLine {
7523                message: "bad\nmessage".into(),
7524            })
7525            .is_err()
7526        );
7527        assert!(read_error_line(&mut &b"0000"[..]).is_err());
7528    }
7529
7530    #[test]
7531    fn refspec_parser_handles_fetch_push_and_negative_forms() {
7532        assert_eq!(
7533            parse_refspec("+refs/heads/*:refs/remotes/origin/*")
7534                .expect("test operation should succeed"),
7535            RefSpec {
7536                force: true,
7537                negative: false,
7538                src: Some("refs/heads/*".into()),
7539                dst: Some("refs/remotes/origin/*".into()),
7540                pattern: true,
7541            }
7542        );
7543        assert_eq!(
7544            parse_refspec("refs/heads/main").expect("test operation should succeed"),
7545            RefSpec {
7546                force: false,
7547                negative: false,
7548                src: Some("refs/heads/main".into()),
7549                dst: None,
7550                pattern: false,
7551            }
7552        );
7553        assert_eq!(
7554            parse_refspec(":refs/heads/topic").expect("test operation should succeed"),
7555            RefSpec {
7556                force: false,
7557                negative: false,
7558                src: None,
7559                dst: Some("refs/heads/topic".into()),
7560                pattern: false,
7561            }
7562        );
7563        assert_eq!(
7564            parse_refspec(":").expect("test operation should succeed"),
7565            RefSpec {
7566                force: false,
7567                negative: false,
7568                src: None,
7569                dst: None,
7570                pattern: false,
7571            }
7572        );
7573        assert_eq!(
7574            parse_refspec("^refs/tags/private/*").expect("test operation should succeed"),
7575            RefSpec {
7576                force: false,
7577                negative: true,
7578                src: Some("refs/tags/private/*".into()),
7579                dst: None,
7580                pattern: true,
7581            }
7582        );
7583    }
7584
7585    #[test]
7586    fn refspec_encode_and_map_sources() {
7587        let pattern = parse_refspec("+refs/heads/*:refs/remotes/origin/*")
7588            .expect("test operation should succeed");
7589        assert_eq!(
7590            encode_refspec(&pattern).expect("test operation should succeed"),
7591            "+refs/heads/*:refs/remotes/origin/*"
7592        );
7593        assert!(
7594            refspec_matches_source(&pattern, "refs/heads/main")
7595                .expect("test operation should succeed")
7596        );
7597        assert_eq!(
7598            refspec_map_source(&pattern, "refs/heads/main").expect("test operation should succeed"),
7599            Some("refs/remotes/origin/main".into())
7600        );
7601        assert_eq!(
7602            refspec_map_source(&pattern, "refs/tags/v1").expect("test operation should succeed"),
7603            None
7604        );
7605
7606        let direct = parse_refspec("HEAD:refs/heads/main").expect("test operation should succeed");
7607        assert_eq!(
7608            encode_refspec(&direct).expect("test operation should succeed"),
7609            "HEAD:refs/heads/main"
7610        );
7611        assert_eq!(
7612            refspec_map_source(&direct, "HEAD").expect("test operation should succeed"),
7613            Some("refs/heads/main".into())
7614        );
7615
7616        let delete = parse_refspec(":refs/heads/old").expect("test operation should succeed");
7617        assert_eq!(
7618            encode_refspec(&delete).expect("test operation should succeed"),
7619            ":refs/heads/old"
7620        );
7621        assert_eq!(
7622            refspec_map_source(&delete, "HEAD").expect("test operation should succeed"),
7623            None
7624        );
7625
7626        let matching = parse_refspec(":").expect("test operation should succeed");
7627        assert_eq!(
7628            encode_refspec(&matching).expect("test operation should succeed"),
7629            ":"
7630        );
7631    }
7632
7633    #[test]
7634    fn refspec_parser_rejects_malformed_values() {
7635        assert!(parse_refspec("").is_err());
7636        assert!(parse_refspec("+^refs/heads/main").is_err());
7637        assert!(parse_refspec("^refs/heads/main:refs/remotes/origin/main").is_err());
7638        assert!(parse_refspec("refs/heads/*:refs/remotes/origin/main").is_err());
7639        assert!(parse_refspec("refs/heads/**:refs/remotes/origin/*").is_err());
7640        assert!(parse_refspec("refs/heads/main:refs/remotes/origin/main:extra").is_err());
7641        assert!(parse_refspec("refs/heads/main\n").is_err());
7642        assert!(
7643            encode_refspec(&RefSpec {
7644                force: false,
7645                negative: false,
7646                src: Some("refs/heads/*".into()),
7647                dst: Some("refs/remotes/origin/main".into()),
7648                pattern: true,
7649            })
7650            .is_err()
7651        );
7652    }
7653
7654    #[test]
7655    fn fetch_head_records_parse_encode_and_describe_refs() {
7656        let first = ObjectId::from_hex(
7657            ObjectFormat::Sha1,
7658            "1111111111111111111111111111111111111111",
7659        )
7660        .expect("test operation should succeed");
7661        let second = ObjectId::from_hex(
7662            ObjectFormat::Sha1,
7663            "2222222222222222222222222222222222222222",
7664        )
7665        .expect("test operation should succeed");
7666        let input = b"1111111111111111111111111111111111111111\t\tbranch 'main' of ../bundle.bdl\n2222222222222222222222222222222222222222\tnot-for-merge\ttag 'v1' of ../bundle.bdl\n";
7667        let records =
7668            parse_fetch_head(ObjectFormat::Sha1, input).expect("test operation should succeed");
7669        assert_eq!(
7670            records,
7671            vec![
7672                FetchHeadRecord {
7673                    oid: first,
7674                    not_for_merge: false,
7675                    description: "branch 'main' of ../bundle.bdl".into(),
7676                },
7677                FetchHeadRecord {
7678                    oid: second,
7679                    not_for_merge: true,
7680                    description: "tag 'v1' of ../bundle.bdl".into(),
7681                },
7682            ]
7683        );
7684        assert_eq!(
7685            encode_fetch_head(&records).expect("test operation should succeed"),
7686            input
7687        );
7688        assert_eq!(
7689            parse_fetch_head(ObjectFormat::Sha1, b"").expect("test operation should succeed"),
7690            Vec::<FetchHeadRecord>::new()
7691        );
7692        assert_eq!(
7693            fetch_head_remote_description("refs/heads/main", "../bundle.bdl")
7694                .expect("test operation should succeed"),
7695            "branch 'main' of ../bundle.bdl"
7696        );
7697        assert_eq!(
7698            fetch_head_remote_description("refs/tags/v1", "../bundle.bdl")
7699                .expect("test operation should succeed"),
7700            "tag 'v1' of ../bundle.bdl"
7701        );
7702        // A bare `HEAD` fetch records just the URL — git emits an empty note.
7703        assert_eq!(
7704            fetch_head_remote_description("HEAD", "../bundle.bdl")
7705                .expect("test operation should succeed"),
7706            "../bundle.bdl"
7707        );
7708    }
7709
7710    #[test]
7711    fn fetch_head_records_streams_round_trip() {
7712        let records = vec![FetchHeadRecord {
7713            oid: ObjectId::from_hex(
7714                ObjectFormat::Sha1,
7715                "1111111111111111111111111111111111111111",
7716            )
7717            .expect("test operation should succeed"),
7718            not_for_merge: false,
7719            description: "branch 'main' of ../bundle.bdl".into(),
7720        }];
7721        let mut encoded = Vec::new();
7722        write_fetch_head(&mut encoded, &records).expect("test operation should succeed");
7723        let mut input = encoded.as_slice();
7724        assert_eq!(
7725            read_fetch_head(ObjectFormat::Sha1, &mut input).expect("test operation should succeed"),
7726            records
7727        );
7728        assert!(input.is_empty());
7729    }
7730
7731    #[test]
7732    fn fetch_head_records_reject_malformed_lines() {
7733        assert!(
7734            parse_fetch_head(
7735                ObjectFormat::Sha1,
7736                b"1111111111111111111111111111111111111111\t\tbranch 'main'"
7737            )
7738            .is_err()
7739        );
7740        assert!(
7741            parse_fetch_head(
7742                ObjectFormat::Sha1,
7743                b"1111111111111111111111111111111111111111\tfor-merge\tbranch 'main'\n"
7744            )
7745            .is_err()
7746        );
7747        assert!(parse_fetch_head(ObjectFormat::Sha1, b"not-a-hash\t\tbranch 'main'\n").is_err());
7748        assert!(
7749            encode_fetch_head(&[FetchHeadRecord {
7750                oid: ObjectId::from_hex(
7751                    ObjectFormat::Sha1,
7752                    "1111111111111111111111111111111111111111"
7753                )
7754                .expect("test operation should succeed"),
7755                not_for_merge: false,
7756                description: "bad\ndescription".into(),
7757            }])
7758            .is_err()
7759        );
7760    }
7761
7762    #[test]
7763    fn fetch_planner_maps_direct_pattern_and_negative_refspecs() {
7764        let main = ObjectId::from_hex(
7765            ObjectFormat::Sha1,
7766            "1111111111111111111111111111111111111111",
7767        )
7768        .expect("test operation should succeed");
7769        let next = ObjectId::from_hex(
7770            ObjectFormat::Sha1,
7771            "2222222222222222222222222222222222222222",
7772        )
7773        .expect("test operation should succeed");
7774        let refs = vec![
7775            RefAdvertisement {
7776                oid: main.clone(),
7777                name: "refs/heads/main".into(),
7778                capabilities: Vec::new(),
7779            },
7780            RefAdvertisement {
7781                oid: next.clone(),
7782                name: "refs/heads/tmp".into(),
7783                capabilities: Vec::new(),
7784            },
7785        ];
7786        let refspecs = vec![
7787            parse_refspec("+refs/heads/*:refs/remotes/origin/*")
7788                .expect("test operation should succeed"),
7789            parse_refspec("^refs/heads/tmp").expect("test operation should succeed"),
7790        ];
7791        assert_eq!(
7792            plan_fetch_ref_updates(&refs, &refspecs, false).expect("test operation should succeed"),
7793            vec![FetchRefUpdate {
7794                src: "refs/heads/main".into(),
7795                dst: Some("refs/remotes/origin/main".into()),
7796                oid: main,
7797                not_for_merge: false,
7798            }]
7799        );
7800    }
7801
7802    #[test]
7803    fn fetch_planner_autofollows_tags_and_builds_fetch_head_records() {
7804        let commit = ObjectId::from_hex(
7805            ObjectFormat::Sha1,
7806            "1111111111111111111111111111111111111111",
7807        )
7808        .expect("test operation should succeed");
7809        let refs = vec![
7810            RefAdvertisement {
7811                oid: commit.clone(),
7812                name: "refs/heads/main".into(),
7813                capabilities: Vec::new(),
7814            },
7815            RefAdvertisement {
7816                oid: commit.clone(),
7817                name: "refs/tags/v1".into(),
7818                capabilities: Vec::new(),
7819            },
7820        ];
7821        let refspecs = vec![
7822            parse_refspec("refs/heads/main:refs/heads/main")
7823                .expect("test operation should succeed"),
7824        ];
7825        let updates =
7826            plan_fetch_ref_updates(&refs, &refspecs, true).expect("test operation should succeed");
7827        assert_eq!(
7828            updates,
7829            vec![
7830                FetchRefUpdate {
7831                    src: "refs/heads/main".into(),
7832                    dst: Some("refs/heads/main".into()),
7833                    oid: commit.clone(),
7834                    not_for_merge: false,
7835                },
7836                FetchRefUpdate {
7837                    src: "refs/tags/v1".into(),
7838                    dst: Some("refs/tags/v1".into()),
7839                    oid: commit.clone(),
7840                    not_for_merge: true,
7841                },
7842            ]
7843        );
7844        assert_eq!(
7845            fetch_ref_updates_to_fetch_head(&updates, "../bundle.bdl")
7846                .expect("test operation should succeed"),
7847            vec![
7848                FetchHeadRecord {
7849                    oid: commit.clone(),
7850                    not_for_merge: false,
7851                    description: "branch 'main' of ../bundle.bdl".into(),
7852                },
7853                FetchHeadRecord {
7854                    oid: commit,
7855                    not_for_merge: true,
7856                    description: "tag 'v1' of ../bundle.bdl".into(),
7857                },
7858            ]
7859        );
7860    }
7861
7862    #[test]
7863    fn fetch_planner_rejects_missing_or_sourceless_refspecs() {
7864        let refs = vec![RefAdvertisement {
7865            oid: ObjectId::from_hex(
7866                ObjectFormat::Sha1,
7867                "1111111111111111111111111111111111111111",
7868            )
7869            .expect("test operation should succeed"),
7870            name: "refs/heads/main".into(),
7871            capabilities: Vec::new(),
7872        }];
7873        assert!(
7874            plan_fetch_ref_updates(
7875                &refs,
7876                &[parse_refspec("refs/heads/missing").expect("test operation should succeed")],
7877                false
7878            )
7879            .is_err()
7880        );
7881        assert!(
7882            plan_fetch_ref_updates(
7883                &refs,
7884                &[parse_refspec(":refs/heads/main").expect("test operation should succeed")],
7885                false
7886            )
7887            .is_err()
7888        );
7889    }
7890
7891    #[test]
7892    fn fetch_planner_sourceless_positive_refspec_returns_err_not_panic() {
7893        // Regression guard for the sley#7 conversion at the `let Some(src) = ..`
7894        // binding: a non-pattern positive refspec with no source must return an
7895        // error, never panic. Construct the malformed RefSpec directly so the
7896        // test pins the converted guard rather than parse_refspec's behavior.
7897        let refs = vec![RefAdvertisement {
7898            oid: ObjectId::from_hex(
7899                ObjectFormat::Sha1,
7900                "1111111111111111111111111111111111111111",
7901            )
7902            .expect("test operation should succeed"),
7903            name: "refs/heads/main".into(),
7904            capabilities: Vec::new(),
7905        }];
7906        let malformed = RefSpec {
7907            force: false,
7908            negative: false,
7909            src: None,
7910            dst: Some("refs/heads/main".into()),
7911            pattern: false,
7912        };
7913        let result = plan_fetch_ref_updates(&refs, &[malformed], false);
7914        assert!(
7915            result.is_err(),
7916            "sourceless positive refspec must yield Err, got {result:?}"
7917        );
7918    }
7919
7920    #[test]
7921    fn push_planner_builds_create_update_delete_and_matching_commands() {
7922        let old = ObjectId::from_hex(
7923            ObjectFormat::Sha1,
7924            "1111111111111111111111111111111111111111",
7925        )
7926        .expect("test operation should succeed");
7927        let new = ObjectId::from_hex(
7928            ObjectFormat::Sha1,
7929            "2222222222222222222222222222222222222222",
7930        )
7931        .expect("test operation should succeed");
7932        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
7933        let local_refs = vec![
7934            PushSourceRef {
7935                oid: new.clone(),
7936                name: "refs/heads/main".into(),
7937            },
7938            PushSourceRef {
7939                oid: new.clone(),
7940                name: "refs/heads/new".into(),
7941            },
7942        ];
7943        let remote_refs = vec![
7944            RefAdvertisement {
7945                oid: old.clone(),
7946                name: "refs/heads/main".into(),
7947                capabilities: Vec::new(),
7948            },
7949            RefAdvertisement {
7950                oid: old.clone(),
7951                name: "refs/heads/old".into(),
7952                capabilities: Vec::new(),
7953            },
7954        ];
7955
7956        assert_eq!(
7957            plan_push_commands(
7958                ObjectFormat::Sha1,
7959                &local_refs,
7960                &remote_refs,
7961                &[parse_refspec("refs/heads/main").expect("test operation should succeed")],
7962            )
7963            .expect("test operation should succeed"),
7964            vec![ReceivePackCommand {
7965                old_id: old.clone(),
7966                new_id: new.clone(),
7967                name: "refs/heads/main".into(),
7968            }]
7969        );
7970        assert_eq!(
7971            plan_push_commands(
7972                ObjectFormat::Sha1,
7973                &local_refs,
7974                &remote_refs,
7975                &[parse_refspec("refs/heads/new:refs/heads/new")
7976                    .expect("test operation should succeed")],
7977            )
7978            .expect("test operation should succeed"),
7979            vec![ReceivePackCommand {
7980                old_id: zero.clone(),
7981                new_id: new.clone(),
7982                name: "refs/heads/new".into(),
7983            }]
7984        );
7985        assert_eq!(
7986            plan_push_commands(
7987                ObjectFormat::Sha1,
7988                &local_refs,
7989                &remote_refs,
7990                &[parse_refspec(":refs/heads/old").expect("test operation should succeed")],
7991            )
7992            .expect("test operation should succeed"),
7993            vec![ReceivePackCommand {
7994                old_id: old.clone(),
7995                new_id: zero,
7996                name: "refs/heads/old".into(),
7997            }]
7998        );
7999        assert_eq!(
8000            plan_push_commands(
8001                ObjectFormat::Sha1,
8002                &local_refs,
8003                &remote_refs,
8004                &[parse_refspec(":").expect("test operation should succeed")],
8005            )
8006            .expect("test operation should succeed"),
8007            vec![ReceivePackCommand {
8008                old_id: old,
8009                new_id: new,
8010                name: "refs/heads/main".into(),
8011            }]
8012        );
8013    }
8014
8015    #[test]
8016    fn push_planner_builds_wildcard_commands_and_rejects_bad_refspecs() {
8017        let new = ObjectId::from_hex(
8018            ObjectFormat::Sha1,
8019            "2222222222222222222222222222222222222222",
8020        )
8021        .expect("test operation should succeed");
8022        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
8023        let local_refs = vec![PushSourceRef {
8024            oid: new.clone(),
8025            name: "refs/heads/topic".into(),
8026        }];
8027        let commands = plan_push_commands(
8028            ObjectFormat::Sha1,
8029            &local_refs,
8030            &[],
8031            &[parse_refspec("refs/heads/*:refs/heads/review/*")
8032                .expect("test operation should succeed")],
8033        )
8034        .expect("test operation should succeed");
8035        assert_eq!(
8036            commands,
8037            vec![ReceivePackCommand {
8038                old_id: zero,
8039                new_id: new,
8040                name: "refs/heads/review/topic".into(),
8041            }]
8042        );
8043        assert!(
8044            plan_push_commands(
8045                ObjectFormat::Sha1,
8046                &local_refs,
8047                &[],
8048                &[parse_refspec("^refs/heads/topic").expect("test operation should succeed")],
8049            )
8050            .is_err()
8051        );
8052        assert!(
8053            plan_push_commands(
8054                ObjectFormat::Sha1,
8055                &local_refs,
8056                &[],
8057                &[parse_refspec(":refs/heads/missing").expect("test operation should succeed")],
8058            )
8059            .is_err()
8060        );
8061    }
8062
8063    #[test]
8064    fn receive_pack_push_request_builder_negotiates_capabilities() {
8065        let old_id = ObjectId::from_hex(
8066            ObjectFormat::Sha1,
8067            "1111111111111111111111111111111111111111",
8068        )
8069        .expect("test operation should succeed");
8070        let new_id = ObjectId::from_hex(
8071            ObjectFormat::Sha1,
8072            "2222222222222222222222222222222222222222",
8073        )
8074        .expect("test operation should succeed");
8075        let features = ReceivePackFeatures {
8076            report_status_v2: true,
8077            atomic: true,
8078            ofs_delta: true,
8079            push_options: true,
8080            side_band_64k: true,
8081            quiet: true,
8082            object_format: Some(ObjectFormat::Sha1),
8083            ..ReceivePackFeatures::default()
8084        };
8085        let request = build_receive_pack_push_request(
8086            &features,
8087            vec![ReceivePackCommand {
8088                old_id,
8089                new_id,
8090                name: "refs/heads/main".into(),
8091            }],
8092            b"PACKdata".to_vec(),
8093            ReceivePackPushRequestOptions {
8094                report_status_v2: true,
8095                atomic: true,
8096                ofs_delta: true,
8097                side_band_64k: true,
8098                quiet: true,
8099                agent: Some("sley/0".into()),
8100                object_format: Some(ObjectFormat::Sha1),
8101                push_options: vec!["ci.skip".into()],
8102                ..ReceivePackPushRequestOptions::default()
8103            },
8104        )
8105        .expect("test operation should succeed");
8106        assert_eq!(
8107            request.commands.capabilities,
8108            vec![
8109                Capability {
8110                    name: "report-status-v2".into(),
8111                    value: None,
8112                },
8113                Capability {
8114                    name: "atomic".into(),
8115                    value: None,
8116                },
8117                Capability {
8118                    name: "ofs-delta".into(),
8119                    value: None,
8120                },
8121                Capability {
8122                    name: "side-band-64k".into(),
8123                    value: None,
8124                },
8125                Capability {
8126                    name: "quiet".into(),
8127                    value: None,
8128                },
8129                Capability {
8130                    name: "agent".into(),
8131                    value: Some("sley/0".into()),
8132                },
8133                Capability {
8134                    name: "object-format".into(),
8135                    value: Some("sha1".into()),
8136                },
8137                Capability {
8138                    name: "push-options".into(),
8139                    value: None,
8140                },
8141            ]
8142        );
8143        assert_eq!(request.push_options, Some(vec!["ci.skip".into()]));
8144        validate_receive_pack_push_request_features(&features, &request)
8145            .expect("test operation should succeed");
8146    }
8147
8148    #[test]
8149    fn receive_pack_push_request_builder_handles_delete_only_and_rejects_unadvertised() {
8150        let old_id = ObjectId::from_hex(
8151            ObjectFormat::Sha1,
8152            "1111111111111111111111111111111111111111",
8153        )
8154        .expect("test operation should succeed");
8155        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
8156        let features = ReceivePackFeatures {
8157            delete_refs: true,
8158            ..ReceivePackFeatures::default()
8159        };
8160        let request = build_receive_pack_push_request(
8161            &features,
8162            vec![ReceivePackCommand {
8163                old_id,
8164                new_id: zero,
8165                name: "refs/heads/old".into(),
8166            }],
8167            Vec::new(),
8168            ReceivePackPushRequestOptions::default(),
8169        )
8170        .expect("test operation should succeed");
8171        assert_eq!(
8172            request.commands.capabilities,
8173            vec![Capability {
8174                name: "delete-refs".into(),
8175                value: None,
8176            }]
8177        );
8178        assert!(request.packfile.is_empty());
8179
8180        assert!(
8181            build_receive_pack_push_request(
8182                &ReceivePackFeatures::default(),
8183                request.commands.commands.clone(),
8184                Vec::new(),
8185                ReceivePackPushRequestOptions::default(),
8186            )
8187            .is_err()
8188        );
8189        assert!(
8190            build_receive_pack_push_request(
8191                &features,
8192                request.commands.commands,
8193                b"PACK".to_vec(),
8194                ReceivePackPushRequestOptions::default(),
8195            )
8196            .is_err()
8197        );
8198        assert!(
8199            build_receive_pack_push_request(
8200                &features,
8201                Vec::new(),
8202                Vec::new(),
8203                ReceivePackPushRequestOptions {
8204                    push_options: vec!["ci.skip".into()],
8205                    ..ReceivePackPushRequestOptions::default()
8206                },
8207            )
8208            .is_err()
8209        );
8210    }
8211
8212    #[test]
8213    fn smart_http_helpers_build_paths_and_content_types() {
8214        let sha1 = ObjectId::from_hex(
8215            ObjectFormat::Sha1,
8216            "1111111111111111111111111111111111111111",
8217        )
8218        .expect("test operation should succeed");
8219        let sha256 = ObjectId::from_hex(
8220            ObjectFormat::Sha256,
8221            "2222222222222222222222222222222222222222222222222222222222222222",
8222        )
8223        .expect("test operation should succeed");
8224        assert_eq!(
8225            smart_http_info_refs_path("/repo.git/", GitService::UploadPack)
8226                .expect("test operation should succeed"),
8227            "/repo.git/info/refs?service=git-upload-pack"
8228        );
8229        assert_eq!(
8230            dumb_http_info_refs_path("/repo.git/").expect("test operation should succeed"),
8231            "/repo.git/info/refs"
8232        );
8233        assert_eq!(
8234            dumb_http_alternates_path("/repo.git").expect("test operation should succeed"),
8235            "/repo.git/objects/info/http-alternates"
8236        );
8237        assert_eq!(
8238            dumb_http_packs_path("/repo.git/").expect("test operation should succeed"),
8239            "/repo.git/objects/info/packs"
8240        );
8241        assert_eq!(
8242            dumb_http_loose_object_path("/repo.git/", &sha1)
8243                .expect("test operation should succeed"),
8244            "/repo.git/objects/11/11111111111111111111111111111111111111"
8245        );
8246        assert_eq!(
8247            dumb_http_loose_object_path("/repo.git/", &sha256)
8248                .expect("test operation should succeed"),
8249            "/repo.git/objects/22/22222222222222222222222222222222222222222222222222222222222222"
8250        );
8251        assert_eq!(
8252            dumb_http_pack_file_path("/repo.git/", &sha1).expect("test operation should succeed"),
8253            "/repo.git/objects/pack/pack-1111111111111111111111111111111111111111.pack"
8254        );
8255        assert_eq!(
8256            dumb_http_pack_index_path("/repo.git/", &sha1).expect("test operation should succeed"),
8257            "/repo.git/objects/pack/pack-1111111111111111111111111111111111111111.idx"
8258        );
8259        assert_eq!(
8260            smart_http_rpc_path("/repo.git", GitService::ReceivePack)
8261                .expect("test operation should succeed"),
8262            "/repo.git/git-receive-pack"
8263        );
8264        assert_eq!(
8265            smart_http_advertisement_content_type(GitService::UploadPack)
8266                .expect("test operation should succeed"),
8267            "application/x-git-upload-pack-advertisement"
8268        );
8269        assert_eq!(
8270            smart_http_rpc_request_content_type(GitService::UploadPack)
8271                .expect("test operation should succeed"),
8272            "application/x-git-upload-pack-request"
8273        );
8274        assert_eq!(
8275            smart_http_rpc_result_content_type(GitService::ReceivePack)
8276                .expect("test operation should succeed"),
8277            "application/x-git-receive-pack-result"
8278        );
8279        assert_eq!(
8280            parse_smart_http_advertisement_content_type(
8281                "Application/X-Git-Upload-Pack-Advertisement"
8282            )
8283            .expect("test operation should succeed"),
8284            GitService::UploadPack
8285        );
8286        assert_eq!(
8287            parse_smart_http_rpc_request_content_type("application/x-git-receive-pack-request")
8288                .expect("test operation should succeed"),
8289            GitService::ReceivePack
8290        );
8291        assert_eq!(
8292            parse_smart_http_rpc_result_content_type("application/x-git-upload-pack-result")
8293                .expect("test operation should succeed"),
8294            GitService::UploadPack
8295        );
8296    }
8297
8298    #[test]
8299    fn smart_http_helpers_reject_invalid_services_paths_and_content_types() {
8300        let oid = ObjectId::from_hex(
8301            ObjectFormat::Sha1,
8302            "1111111111111111111111111111111111111111",
8303        )
8304        .expect("test operation should succeed");
8305        assert!(smart_http_info_refs_path("repo.git", GitService::UploadPack).is_err());
8306        assert!(smart_http_rpc_path("/repo.git?x=1", GitService::UploadPack).is_err());
8307        assert!(dumb_http_info_refs_path("repo.git").is_err());
8308        assert!(dumb_http_alternates_path("/repo.git#fragment").is_err());
8309        assert!(dumb_http_packs_path("/repo.git?query").is_err());
8310        assert!(dumb_http_loose_object_path("repo.git", &oid).is_err());
8311        assert!(dumb_http_pack_file_path("/repo.git#fragment", &oid).is_err());
8312        assert!(dumb_http_pack_index_path("/repo.git?query", &oid).is_err());
8313        assert!(smart_http_info_refs_path("/repo.git", GitService::UploadArchive).is_err());
8314        assert!(smart_http_advertisement_content_type(GitService::UploadArchive).is_err());
8315        assert!(
8316            parse_smart_http_advertisement_content_type(
8317                "application/x-git-upload-archive-advertisement"
8318            )
8319            .is_err()
8320        );
8321        assert!(
8322            parse_smart_http_rpc_request_content_type("application/x-git-upload-pack-result")
8323                .is_err()
8324        );
8325        assert!(
8326            parse_smart_http_rpc_result_content_type(
8327                "application/x-git-receive-pack-result; charset=utf-8"
8328            )
8329            .is_err()
8330        );
8331    }
8332
8333    #[test]
8334    fn sideband_packets_parse_and_encode_channels() {
8335        let payloads = vec![
8336            b"\x01PACK bytes".to_vec(),
8337            b"\x02counting objects\n".to_vec(),
8338            b"\x03fatal error\n".to_vec(),
8339        ];
8340        let packets = parse_sideband_packets(&payloads).expect("test operation should succeed");
8341        assert_eq!(
8342            packets,
8343            vec![
8344                SideBandPacket {
8345                    channel: SideBandChannel::Data,
8346                    data: b"PACK bytes".to_vec(),
8347                },
8348                SideBandPacket {
8349                    channel: SideBandChannel::Progress,
8350                    data: b"counting objects\n".to_vec(),
8351                },
8352                SideBandPacket {
8353                    channel: SideBandChannel::Fatal,
8354                    data: b"fatal error\n".to_vec(),
8355                },
8356            ]
8357        );
8358        assert_eq!(
8359            encode_sideband_packets(&packets).expect("test operation should succeed"),
8360            payloads
8361        );
8362    }
8363
8364    #[test]
8365    fn sideband_stream_parses_encodes_and_demuxes_packets() {
8366        let frames = vec![
8367            PktLineFrame::Data(vec![1, b'P', b'A']),
8368            PktLineFrame::Data(vec![2, b'c', b'o', b'u', b'n', b't', b'\n']),
8369            PktLineFrame::Data(vec![1, b'C', b'K']),
8370            PktLineFrame::Flush,
8371        ];
8372        let packets = parse_sideband_stream(&frames).expect("test operation should succeed");
8373        assert_eq!(
8374            packets,
8375            vec![
8376                SideBandPacket {
8377                    channel: SideBandChannel::Data,
8378                    data: b"PA".to_vec(),
8379                },
8380                SideBandPacket {
8381                    channel: SideBandChannel::Progress,
8382                    data: b"count\n".to_vec(),
8383                },
8384                SideBandPacket {
8385                    channel: SideBandChannel::Data,
8386                    data: b"CK".to_vec(),
8387                },
8388            ]
8389        );
8390        assert_eq!(
8391            encode_sideband_stream(&packets).expect("test operation should succeed"),
8392            frames
8393        );
8394        assert_eq!(
8395            demux_sideband_stream(&frames).expect("test operation should succeed"),
8396            SideBandDemux {
8397                data: b"PACK".to_vec(),
8398                progress: vec![b"count\n".to_vec()],
8399            }
8400        );
8401    }
8402
8403    #[test]
8404    fn sideband_stream_reads_and_writes_until_flush() {
8405        let packets = vec![
8406            SideBandPacket {
8407                channel: SideBandChannel::Data,
8408                data: b"PACK".to_vec(),
8409            },
8410            SideBandPacket {
8411                channel: SideBandChannel::Progress,
8412                data: b"done\n".to_vec(),
8413            },
8414        ];
8415        let mut encoded = Vec::new();
8416        write_sideband_stream(&mut encoded, &packets).expect("test operation should succeed");
8417        encoded.extend_from_slice(b"tail");
8418
8419        let mut input = encoded.as_slice();
8420        assert_eq!(
8421            read_sideband_stream(&mut input).expect("test operation should succeed"),
8422            packets
8423        );
8424        assert_eq!(input, b"tail");
8425
8426        let mut input = encoded.as_slice();
8427        assert_eq!(
8428            read_and_demux_sideband_stream(&mut input).expect("test operation should succeed"),
8429            SideBandDemux {
8430                data: b"PACK".to_vec(),
8431                progress: vec![b"done\n".to_vec()],
8432            }
8433        );
8434        assert_eq!(input, b"tail");
8435    }
8436
8437    #[test]
8438    fn sideband_packets_demux_data_and_progress() {
8439        let payloads = vec![
8440            b"\x01PACK".to_vec(),
8441            b"\x02counting objects\n".to_vec(),
8442            b"\x01 bytes".to_vec(),
8443            b"\x02done\n".to_vec(),
8444        ];
8445        assert_eq!(
8446            parse_and_demux_sideband_packets(&payloads).expect("test operation should succeed"),
8447            SideBandDemux {
8448                data: b"PACK bytes".to_vec(),
8449                progress: vec![b"counting objects\n".to_vec(), b"done\n".to_vec()],
8450            }
8451        );
8452    }
8453
8454    #[test]
8455    fn sideband_packets_reject_bad_channels_and_oversize_payloads() {
8456        assert!(parse_sideband_packet(b"").is_err());
8457        assert!(parse_sideband_packet(b"\x04bad").is_err());
8458        assert!(
8459            parse_sideband_stream(&[PktLineFrame::Data(vec![1, b'P', b'A', b'C', b'K'])]).is_err()
8460        );
8461        assert!(parse_sideband_stream(&[PktLineFrame::Delimiter, PktLineFrame::Flush]).is_err());
8462        assert!(
8463            parse_sideband_stream(&[
8464                PktLineFrame::Data(vec![1, b'P', b'A']),
8465                PktLineFrame::Flush,
8466                PktLineFrame::Data(vec![1, b'C', b'K']),
8467            ])
8468            .is_err()
8469        );
8470        assert!(
8471            parse_sideband_stream(&[
8472                PktLineFrame::Data(vec![1, b'P', b'A']),
8473                PktLineFrame::Data(b"\x04bad".to_vec()),
8474                PktLineFrame::Flush,
8475            ])
8476            .is_err()
8477        );
8478        assert!(
8479            encode_sideband_packet(&SideBandPacket {
8480                channel: SideBandChannel::Data,
8481                data: vec![0; PKT_LINE_MAX_PAYLOAD_LEN],
8482            })
8483            .is_err()
8484        );
8485        assert!(
8486            demux_sideband_packets(&[SideBandPacket {
8487                channel: SideBandChannel::Fatal,
8488                data: b"remote died\n".to_vec(),
8489            }])
8490            .is_err()
8491        );
8492    }
8493
8494    #[test]
8495    fn upload_archive_request_parses_and_encodes_arguments() {
8496        let frames = vec![
8497            PktLineFrame::Data(b"argument --format=tar\n".to_vec()),
8498            PktLineFrame::Data(b"argument HEAD:dir with spaces\n".to_vec()),
8499            PktLineFrame::Flush,
8500        ];
8501        let request = parse_upload_archive_request(&frames).expect("test operation should succeed");
8502        assert_eq!(
8503            request,
8504            UploadArchiveRequest {
8505                arguments: vec!["--format=tar".into(), "HEAD:dir with spaces".into()],
8506            }
8507        );
8508        assert_eq!(
8509            encode_upload_archive_request(&request).expect("test operation should succeed"),
8510            frames
8511        );
8512    }
8513
8514    #[test]
8515    fn upload_archive_request_streams_round_trip() {
8516        let request = UploadArchiveRequest {
8517            arguments: vec!["--prefix=src/".into(), "main".into()],
8518        };
8519        let mut encoded = Vec::new();
8520        write_upload_archive_request(&mut encoded, &request)
8521            .expect("test operation should succeed");
8522        encoded.extend_from_slice(b"tail");
8523
8524        let mut input = encoded.as_slice();
8525        assert_eq!(
8526            read_upload_archive_request(&mut input).expect("test operation should succeed"),
8527            request
8528        );
8529        assert_eq!(input, b"tail");
8530    }
8531
8532    #[test]
8533    fn upload_archive_request_rejects_malformed_streams() {
8534        assert!(parse_upload_archive_request(&[PktLineFrame::Flush]).is_err());
8535        assert!(
8536            parse_upload_archive_request(&[
8537                PktLineFrame::Data(b"--format=tar\n".to_vec()),
8538                PktLineFrame::Flush,
8539            ])
8540            .is_err()
8541        );
8542        assert!(
8543            parse_upload_archive_request(&[
8544                PktLineFrame::Data(b"argument HEAD\n".to_vec()),
8545                PktLineFrame::Delimiter,
8546                PktLineFrame::Flush,
8547            ])
8548            .is_err()
8549        );
8550        assert!(
8551            encode_upload_archive_request(&UploadArchiveRequest {
8552                arguments: vec!["bad\narg".into()],
8553            })
8554            .is_err()
8555        );
8556    }
8557
8558    #[test]
8559    fn upload_archive_response_parses_ack_sideband_and_nack() {
8560        let ack_frames = vec![
8561            PktLineFrame::Data(b"ACK\n".to_vec()),
8562            PktLineFrame::Data(b"\x01tar bytes".to_vec()),
8563            PktLineFrame::Data(b"\x02progress\n".to_vec()),
8564            PktLineFrame::Flush,
8565        ];
8566        let response =
8567            parse_upload_archive_response(&ack_frames).expect("test operation should succeed");
8568        assert_eq!(
8569            response,
8570            UploadArchiveResponse::Ack {
8571                sideband: vec![
8572                    SideBandPacket {
8573                        channel: SideBandChannel::Data,
8574                        data: b"tar bytes".to_vec(),
8575                    },
8576                    SideBandPacket {
8577                        channel: SideBandChannel::Progress,
8578                        data: b"progress\n".to_vec(),
8579                    },
8580                ],
8581            }
8582        );
8583        assert_eq!(
8584            encode_upload_archive_response(&response).expect("test operation should succeed"),
8585            ack_frames
8586        );
8587        assert_eq!(
8588            demux_upload_archive_response(&response).expect("test operation should succeed"),
8589            SideBandDemux {
8590                data: b"tar bytes".to_vec(),
8591                progress: vec![b"progress\n".to_vec()],
8592            }
8593        );
8594
8595        let nack = UploadArchiveResponse::Nack {
8596            message: "unreachable tree".into(),
8597        };
8598        let nack_frames = vec![
8599            PktLineFrame::Data(b"NACK unreachable tree\n".to_vec()),
8600            PktLineFrame::Flush,
8601        ];
8602        assert_eq!(
8603            parse_upload_archive_response(&nack_frames).expect("test operation should succeed"),
8604            nack
8605        );
8606        assert_eq!(
8607            encode_upload_archive_response(&nack).expect("test operation should succeed"),
8608            nack_frames
8609        );
8610        assert!(demux_upload_archive_response(&nack).is_err());
8611    }
8612
8613    #[test]
8614    fn upload_archive_response_streams_round_trip() {
8615        let response = UploadArchiveResponse::Ack {
8616            sideband: vec![SideBandPacket {
8617                channel: SideBandChannel::Data,
8618                data: b"tar bytes".to_vec(),
8619            }],
8620        };
8621        let mut encoded = Vec::new();
8622        write_upload_archive_response(&mut encoded, &response)
8623            .expect("test operation should succeed");
8624        encoded.extend_from_slice(b"tail");
8625
8626        let mut input = encoded.as_slice();
8627        assert_eq!(
8628            read_upload_archive_response(&mut input).expect("test operation should succeed"),
8629            response
8630        );
8631        assert_eq!(input, b"tail");
8632    }
8633
8634    #[test]
8635    fn upload_archive_response_rejects_malformed_streams() {
8636        assert!(parse_upload_archive_response(&[]).is_err());
8637        assert!(
8638            parse_upload_archive_response(&[
8639                PktLineFrame::Data(b"ACK\n".to_vec()),
8640                PktLineFrame::Flush,
8641                PktLineFrame::Data(b"\x01tail".to_vec()),
8642            ])
8643            .is_err()
8644        );
8645        assert!(
8646            parse_upload_archive_response(&[
8647                PktLineFrame::Data(b"NACK\n".to_vec()),
8648                PktLineFrame::Flush,
8649            ])
8650            .is_err()
8651        );
8652        assert!(
8653            parse_upload_archive_response(&[
8654                PktLineFrame::Data(b"NACK denied\n".to_vec()),
8655                PktLineFrame::Data(b"\x02extra\n".to_vec()),
8656                PktLineFrame::Flush,
8657            ])
8658            .is_err()
8659        );
8660        assert!(
8661            encode_upload_archive_response(&UploadArchiveResponse::Nack {
8662                message: "bad\nmessage".into(),
8663            })
8664            .is_err()
8665        );
8666    }
8667
8668    #[test]
8669    fn capabilities_parse_and_encode_tokens() {
8670        let capabilities = parse_capabilities(
8671            b"multi_ack thin-pack agent=git/2.54.0 symref=HEAD:refs/heads/main\n",
8672        )
8673        .expect("test operation should succeed");
8674        assert_eq!(
8675            capabilities,
8676            vec![
8677                Capability {
8678                    name: "multi_ack".into(),
8679                    value: None,
8680                },
8681                Capability {
8682                    name: "thin-pack".into(),
8683                    value: None,
8684                },
8685                Capability {
8686                    name: "agent".into(),
8687                    value: Some("git/2.54.0".into()),
8688                },
8689                Capability {
8690                    name: "symref".into(),
8691                    value: Some("HEAD:refs/heads/main".into()),
8692                },
8693            ]
8694        );
8695        assert_eq!(
8696            encode_capabilities(&capabilities).expect("test operation should succeed"),
8697            b"multi_ack thin-pack agent=git/2.54.0 symref=HEAD:refs/heads/main"
8698        );
8699    }
8700
8701    #[test]
8702    fn capabilities_reject_empty_or_delimited_fields() {
8703        assert!(parse_capabilities(b"multi_ack  thin-pack").is_err());
8704        assert!(parse_capabilities(b"agent=").is_err());
8705        assert!(parse_capabilities(b"symref=HEAD:refs/heads/main\nbad").is_err());
8706        assert!(
8707            encode_capabilities(&[Capability {
8708                name: "bad name".into(),
8709                value: None,
8710            }])
8711            .is_err()
8712        );
8713    }
8714
8715    #[test]
8716    fn protocol_v2_object_format_uses_capability_or_defaults_to_sha1() {
8717        assert_eq!(
8718            protocol_v2_object_format(&[]).expect("test operation should succeed"),
8719            ObjectFormat::Sha1
8720        );
8721        assert_eq!(
8722            protocol_v2_object_format(&[Capability {
8723                name: "object-format".into(),
8724                value: Some("sha256".into()),
8725            }])
8726            .expect("test operation should succeed"),
8727            ObjectFormat::Sha256
8728        );
8729        assert!(
8730            protocol_v2_object_format(&[Capability {
8731                name: "object-format".into(),
8732                value: None,
8733            }])
8734            .is_err()
8735        );
8736        assert!(
8737            protocol_v2_object_format(&[
8738                Capability {
8739                    name: "object-format".into(),
8740                    value: Some("sha1".into()),
8741                },
8742                Capability {
8743                    name: "object-format".into(),
8744                    value: Some("sha256".into()),
8745                },
8746            ])
8747            .is_err()
8748        );
8749        assert!(
8750            protocol_v2_object_format(&[Capability {
8751                name: "object-format".into(),
8752                value: Some("unknown".into()),
8753            }])
8754            .is_err()
8755        );
8756    }
8757
8758    #[test]
8759    fn protocol_v2_command_request_capabilities_validate_against_handshake() {
8760        let handshake = TransportHandshake {
8761            protocol: ProtocolVersion::V2,
8762            capabilities: vec![
8763                Capability {
8764                    name: "fetch".into(),
8765                    value: Some("shallow filter".into()),
8766                },
8767                Capability {
8768                    name: "agent".into(),
8769                    value: Some("sley/0".into()),
8770                },
8771                Capability {
8772                    name: "object-format".into(),
8773                    value: Some("sha1".into()),
8774                },
8775            ],
8776        };
8777        validate_protocol_v2_command_request_capabilities(
8778            &handshake,
8779            &ProtocolV2CommandRequest {
8780                command: "fetch".into(),
8781                capabilities: vec![
8782                    Capability {
8783                        name: "agent".into(),
8784                        value: Some("client/1".into()),
8785                    },
8786                    Capability {
8787                        name: "object-format".into(),
8788                        value: Some("sha1".into()),
8789                    },
8790                ],
8791                arguments: Vec::new(),
8792            },
8793        )
8794        .expect("test operation should succeed");
8795        assert!(
8796            validate_protocol_v2_command_request_capabilities(
8797                &handshake,
8798                &ProtocolV2CommandRequest {
8799                    command: "ls-refs".into(),
8800                    capabilities: Vec::new(),
8801                    arguments: Vec::new(),
8802                },
8803            )
8804            .is_err()
8805        );
8806        assert!(
8807            validate_protocol_v2_command_request_capabilities(
8808                &handshake,
8809                &ProtocolV2CommandRequest {
8810                    command: "fetch".into(),
8811                    capabilities: vec![Capability {
8812                        name: "server-option".into(),
8813                        value: None,
8814                    }],
8815                    arguments: Vec::new(),
8816                },
8817            )
8818            .is_err()
8819        );
8820        assert!(
8821            validate_protocol_v2_command_request_capabilities(
8822                &handshake,
8823                &ProtocolV2CommandRequest {
8824                    command: "fetch".into(),
8825                    capabilities: vec![Capability {
8826                        name: "object-format".into(),
8827                        value: Some("sha256".into()),
8828                    }],
8829                    arguments: Vec::new(),
8830                },
8831            )
8832            .is_err()
8833        );
8834        assert!(
8835            validate_protocol_v2_command_request_capabilities(
8836                &handshake,
8837                &ProtocolV2CommandRequest {
8838                    command: "fetch".into(),
8839                    capabilities: vec![Capability {
8840                        name: "agent".into(),
8841                        value: None,
8842                    }],
8843                    arguments: Vec::new(),
8844                },
8845            )
8846            .is_err()
8847        );
8848    }
8849
8850    #[test]
8851    fn protocol_v2_command_options_parse_and_encode_known_capabilities() {
8852        let capabilities = vec![
8853            Capability {
8854                name: "agent".into(),
8855                value: Some("sley/0".into()),
8856            },
8857            Capability {
8858                name: "object-format".into(),
8859                value: Some("sha256".into()),
8860            },
8861            Capability {
8862                name: "server-option".into(),
8863                value: Some("trace=true".into()),
8864            },
8865            Capability {
8866                name: "server-option".into(),
8867                value: Some("region=west".into()),
8868            },
8869            Capability {
8870                name: "session-id".into(),
8871                value: Some("abc123".into()),
8872            },
8873        ];
8874        let options = parse_protocol_v2_command_options(&capabilities)
8875            .expect("test operation should succeed");
8876        assert_eq!(
8877            options,
8878            ProtocolV2CommandOptions {
8879                agent: Some("sley/0".into()),
8880                object_format: Some(ObjectFormat::Sha256),
8881                server_options: vec!["trace=true".into(), "region=west".into()],
8882                extra: vec![Capability {
8883                    name: "session-id".into(),
8884                    value: Some("abc123".into()),
8885                }],
8886            }
8887        );
8888        assert_eq!(
8889            encode_protocol_v2_command_options(&options).expect("test operation should succeed"),
8890            capabilities
8891        );
8892    }
8893
8894    #[test]
8895    fn protocol_v2_command_options_reject_malformed_known_capabilities() {
8896        assert!(
8897            parse_protocol_v2_command_options(&[
8898                Capability {
8899                    name: "agent".into(),
8900                    value: Some("sley/0".into()),
8901                },
8902                Capability {
8903                    name: "agent".into(),
8904                    value: Some("sley/1".into()),
8905                },
8906            ])
8907            .is_err()
8908        );
8909        assert!(
8910            parse_protocol_v2_command_options(&[Capability {
8911                name: "object-format".into(),
8912                value: Some("sha512".into()),
8913            }])
8914            .is_err()
8915        );
8916        assert!(
8917            parse_protocol_v2_command_options(&[Capability {
8918                name: "server-option".into(),
8919                value: None,
8920            }])
8921            .is_err()
8922        );
8923        assert!(
8924            encode_protocol_v2_command_options(&ProtocolV2CommandOptions {
8925                extra: vec![Capability {
8926                    name: "server-option".into(),
8927                    value: Some("trace=true".into()),
8928                }],
8929                ..ProtocolV2CommandOptions::default()
8930            })
8931            .is_err()
8932        );
8933    }
8934
8935    #[test]
8936    fn protocol_v2_ls_refs_features_parse_and_encode_advertisement() {
8937        let capabilities = vec![Capability {
8938            name: "ls-refs".into(),
8939            value: Some("unborn custom".into()),
8940        }];
8941        let features = parse_protocol_v2_ls_refs_features(&capabilities)
8942            .expect("test operation should succeed")
8943            .expect("test operation should succeed");
8944        assert_eq!(
8945            features,
8946            ProtocolV2LsRefsFeatures {
8947                unborn: true,
8948                unknown: vec!["custom".into()],
8949            }
8950        );
8951        assert_eq!(
8952            encode_protocol_v2_ls_refs_capability(&features)
8953                .expect("test operation should succeed"),
8954            capabilities[0]
8955        );
8956        assert_eq!(
8957            parse_protocol_v2_ls_refs_features(&[Capability {
8958                name: "ls-refs".into(),
8959                value: None,
8960            }])
8961            .expect("test operation should succeed")
8962            .expect("test operation should succeed"),
8963            ProtocolV2LsRefsFeatures::default()
8964        );
8965        assert!(
8966            parse_protocol_v2_ls_refs_features(&[Capability {
8967                name: "fetch".into(),
8968                value: Some("filter".into()),
8969            }])
8970            .expect("test operation should succeed")
8971            .is_none()
8972        );
8973    }
8974
8975    #[test]
8976    fn protocol_v2_ls_refs_features_reject_malformed_advertisements() {
8977        assert!(
8978            parse_protocol_v2_ls_refs_features(&[
8979                Capability {
8980                    name: "ls-refs".into(),
8981                    value: None,
8982                },
8983                Capability {
8984                    name: "ls-refs".into(),
8985                    value: None,
8986                },
8987            ])
8988            .is_err()
8989        );
8990        assert!(
8991            parse_protocol_v2_ls_refs_features(&[Capability {
8992                name: "ls-refs".into(),
8993                value: Some("unborn  custom".into()),
8994            }])
8995            .is_err()
8996        );
8997        assert!(
8998            encode_protocol_v2_ls_refs_capability(&ProtocolV2LsRefsFeatures {
8999                unknown: vec!["unborn".into()],
9000                ..ProtocolV2LsRefsFeatures::default()
9001            })
9002            .is_err()
9003        );
9004    }
9005
9006    #[test]
9007    fn protocol_v2_ls_refs_command_request_validates_unborn_feature() {
9008        let handshake = TransportHandshake {
9009            protocol: ProtocolVersion::V2,
9010            capabilities: vec![Capability {
9011                name: "ls-refs".into(),
9012                value: Some("unborn".into()),
9013            }],
9014        };
9015        let request = ProtocolV2CommandRequest {
9016            command: "ls-refs".into(),
9017            capabilities: Vec::new(),
9018            arguments: vec![b"unborn".to_vec(), b"ref-prefix HEAD".to_vec()],
9019        };
9020        let parsed = validate_protocol_v2_ls_refs_command_request(&handshake, &request)
9021            .expect("test operation should succeed");
9022        assert!(parsed.unborn);
9023        assert_eq!(parsed.ref_prefixes, vec!["HEAD"]);
9024
9025        let blocked = TransportHandshake {
9026            protocol: ProtocolVersion::V2,
9027            capabilities: vec![Capability {
9028                name: "ls-refs".into(),
9029                value: None,
9030            }],
9031        };
9032        assert!(validate_protocol_v2_ls_refs_command_request(&blocked, &request).is_err());
9033    }
9034
9035    #[test]
9036    fn protocol_v2_fetch_features_parse_and_encode_advertisement() {
9037        let capabilities = vec![Capability {
9038            name: "fetch".into(),
9039            value: Some(
9040                "shallow wait-for-done filter ref-in-want sideband-all packfile-uris custom".into(),
9041            ),
9042        }];
9043        let features = parse_protocol_v2_fetch_features(&capabilities)
9044            .expect("test operation should succeed")
9045            .expect("test operation should succeed");
9046        assert_eq!(
9047            features,
9048            ProtocolV2FetchFeatures {
9049                shallow: true,
9050                wait_for_done: true,
9051                filter: true,
9052                ref_in_want: true,
9053                sideband_all: true,
9054                packfile_uris: true,
9055                unknown: vec!["custom".into()],
9056            }
9057        );
9058        assert_eq!(
9059            encode_protocol_v2_fetch_capability(&features).expect("test operation should succeed"),
9060            capabilities[0]
9061        );
9062        assert_eq!(
9063            parse_protocol_v2_fetch_features(&[Capability {
9064                name: "fetch".into(),
9065                value: None,
9066            }])
9067            .expect("test operation should succeed")
9068            .expect("test operation should succeed"),
9069            ProtocolV2FetchFeatures::default()
9070        );
9071        assert!(
9072            parse_protocol_v2_fetch_features(&[])
9073                .expect("test operation should succeed")
9074                .is_none()
9075        );
9076    }
9077
9078    #[test]
9079    fn protocol_v2_fetch_features_reject_malformed_advertisements() {
9080        assert!(
9081            parse_protocol_v2_fetch_features(&[
9082                Capability {
9083                    name: "fetch".into(),
9084                    value: None,
9085                },
9086                Capability {
9087                    name: "fetch".into(),
9088                    value: None,
9089                },
9090            ])
9091            .is_err()
9092        );
9093        assert!(
9094            parse_protocol_v2_fetch_features(&[Capability {
9095                name: "fetch".into(),
9096                value: Some("filter  shallow".into()),
9097            }])
9098            .is_err()
9099        );
9100        assert!(
9101            encode_protocol_v2_fetch_capability(&ProtocolV2FetchFeatures {
9102                unknown: vec!["filter".into()],
9103                ..ProtocolV2FetchFeatures::default()
9104            })
9105            .is_err()
9106        );
9107    }
9108
9109    #[test]
9110    fn protocol_v2_fetch_request_features_validate_feature_gated_arguments() {
9111        let features = ProtocolV2FetchFeatures {
9112            shallow: true,
9113            wait_for_done: true,
9114            filter: true,
9115            ref_in_want: true,
9116            sideband_all: true,
9117            packfile_uris: true,
9118            unknown: Vec::new(),
9119        };
9120        validate_protocol_v2_fetch_request_features(
9121            &features,
9122            &ProtocolV2FetchRequest {
9123                want_refs: vec!["refs/heads/main".into()],
9124                shallow: vec![
9125                    ObjectId::from_hex(
9126                        ObjectFormat::Sha1,
9127                        "1111111111111111111111111111111111111111",
9128                    )
9129                    .expect("test operation should succeed"),
9130                ],
9131                deepen: Some(1),
9132                filter: Some("blob:none".into()),
9133                packfile_uris: Some("https".into()),
9134                sideband_all: true,
9135                wait_for_done: true,
9136                ..ProtocolV2FetchRequest::default()
9137            },
9138        )
9139        .expect("test operation should succeed");
9140
9141        let request = ProtocolV2FetchRequest {
9142            want_refs: vec!["refs/heads/main".into()],
9143            filter: Some("blob:none".into()),
9144            sideband_all: true,
9145            ..ProtocolV2FetchRequest::default()
9146        };
9147        assert!(
9148            validate_protocol_v2_fetch_request_features(
9149                &ProtocolV2FetchFeatures::default(),
9150                &request,
9151            )
9152            .is_err()
9153        );
9154        assert!(
9155            validate_protocol_v2_fetch_request_features(
9156                &ProtocolV2FetchFeatures {
9157                    ref_in_want: true,
9158                    filter: true,
9159                    ..ProtocolV2FetchFeatures::default()
9160                },
9161                &request,
9162            )
9163            .is_err()
9164        );
9165    }
9166
9167    #[test]
9168    fn protocol_v2_fetch_command_request_validates_against_handshake_features() {
9169        let handshake = TransportHandshake {
9170            protocol: ProtocolVersion::V2,
9171            capabilities: vec![
9172                Capability {
9173                    name: "fetch".into(),
9174                    value: Some("filter ref-in-want".into()),
9175                },
9176                Capability {
9177                    name: "agent".into(),
9178                    value: Some("sley/0".into()),
9179                },
9180            ],
9181        };
9182        let request = ProtocolV2CommandRequest {
9183            command: "fetch".into(),
9184            capabilities: vec![Capability {
9185                name: "agent".into(),
9186                value: Some("client/1".into()),
9187            }],
9188            arguments: vec![
9189                b"want-ref refs/heads/main".to_vec(),
9190                b"filter blob:none".to_vec(),
9191            ],
9192        };
9193        let fetch =
9194            validate_protocol_v2_fetch_command_request(&handshake, ObjectFormat::Sha1, &request)
9195                .expect("test operation should succeed");
9196        assert_eq!(fetch.want_refs, vec!["refs/heads/main"]);
9197        assert_eq!(fetch.filter.as_deref(), Some("blob:none"));
9198
9199        let mut bad = request.clone();
9200        bad.arguments.push(b"sideband-all".to_vec());
9201        assert!(
9202            validate_protocol_v2_fetch_command_request(&handshake, ObjectFormat::Sha1, &bad)
9203                .is_err()
9204        );
9205    }
9206
9207    #[test]
9208    fn protocol_v2_object_info_request_parses_encodes_and_validates() {
9209        let oid = ObjectId::from_hex(
9210            ObjectFormat::Sha1,
9211            "1111111111111111111111111111111111111111",
9212        )
9213        .expect("test operation should succeed");
9214        let request = ProtocolV2CommandRequest {
9215            command: "object-info".into(),
9216            capabilities: Vec::new(),
9217            arguments: vec![
9218                b"size".to_vec(),
9219                b"oid 1111111111111111111111111111111111111111".to_vec(),
9220            ],
9221        };
9222        let parsed =
9223            ProtocolV2ObjectInfoRequest::from_command_request(ObjectFormat::Sha1, &request)
9224                .expect("test operation should succeed");
9225        assert_eq!(
9226            parsed,
9227            ProtocolV2ObjectInfoRequest {
9228                size: true,
9229                oids: vec![oid],
9230            }
9231        );
9232        assert_eq!(
9233            parsed
9234                .to_command_request()
9235                .expect("test operation should succeed"),
9236            request
9237        );
9238
9239        let handshake = TransportHandshake {
9240            protocol: ProtocolVersion::V2,
9241            capabilities: vec![Capability {
9242                name: "object-info".into(),
9243                value: None,
9244            }],
9245        };
9246        assert_eq!(
9247            validate_protocol_v2_object_info_command_request(
9248                &handshake,
9249                ObjectFormat::Sha1,
9250                &request,
9251            )
9252            .expect("test operation should succeed"),
9253            parsed
9254        );
9255    }
9256
9257    #[test]
9258    fn protocol_v2_object_info_request_streams_round_trip() {
9259        let request = 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        let mut encoded = Vec::new();
9270        write_protocol_v2_object_info_request(&mut encoded, &request)
9271            .expect("test operation should succeed");
9272        encoded.extend_from_slice(b"tail");
9273
9274        let mut input = encoded.as_slice();
9275        assert_eq!(
9276            read_protocol_v2_object_info_request(ObjectFormat::Sha1, &mut input)
9277                .expect("test operation should succeed"),
9278            request
9279        );
9280        assert_eq!(input, b"tail");
9281    }
9282
9283    #[test]
9284    fn protocol_v2_object_info_request_rejects_malformed_arguments() {
9285        assert!(
9286            ProtocolV2ObjectInfoRequest::from_command_request(
9287                ObjectFormat::Sha1,
9288                &ProtocolV2CommandRequest {
9289                    command: "object-info".into(),
9290                    capabilities: Vec::new(),
9291                    arguments: vec![b"oid 1111111111111111111111111111111111111111".to_vec()],
9292                },
9293            )
9294            .is_err()
9295        );
9296        assert!(
9297            ProtocolV2ObjectInfoRequest::from_command_request(
9298                ObjectFormat::Sha1,
9299                &ProtocolV2CommandRequest {
9300                    command: "object-info".into(),
9301                    capabilities: Vec::new(),
9302                    arguments: vec![b"size".to_vec(), b"size".to_vec()],
9303                },
9304            )
9305            .is_err()
9306        );
9307        assert!(
9308            ProtocolV2ObjectInfoRequest::from_command_request(
9309                ObjectFormat::Sha1,
9310                &ProtocolV2CommandRequest {
9311                    command: "object-info".into(),
9312                    capabilities: Vec::new(),
9313                    arguments: vec![b"size".to_vec()],
9314                },
9315            )
9316            .is_err()
9317        );
9318        assert!(
9319            ProtocolV2ObjectInfoRequest::from_command_request(
9320                ObjectFormat::Sha1,
9321                &ProtocolV2CommandRequest {
9322                    command: "object-info".into(),
9323                    capabilities: Vec::new(),
9324                    arguments: vec![b"size".to_vec(), b"oid not-an-oid".to_vec()],
9325                },
9326            )
9327            .is_err()
9328        );
9329        assert!(
9330            validate_protocol_v2_object_info_command_request(
9331                &TransportHandshake {
9332                    protocol: ProtocolVersion::V2,
9333                    capabilities: Vec::new(),
9334                },
9335                ObjectFormat::Sha1,
9336                &ProtocolV2CommandRequest {
9337                    command: "object-info".into(),
9338                    capabilities: Vec::new(),
9339                    arguments: vec![
9340                        b"size".to_vec(),
9341                        b"oid 1111111111111111111111111111111111111111".to_vec(),
9342                    ],
9343                },
9344            )
9345            .is_err()
9346        );
9347    }
9348
9349    #[test]
9350    fn protocol_v2_command_request_classifies_known_and_unknown_commands() {
9351        let handshake = TransportHandshake {
9352            protocol: ProtocolVersion::V2,
9353            capabilities: vec![
9354                Capability {
9355                    name: "ls-refs".into(),
9356                    value: Some("unborn".into()),
9357                },
9358                Capability {
9359                    name: "fetch".into(),
9360                    value: Some("filter ref-in-want".into()),
9361                },
9362                Capability {
9363                    name: "object-info".into(),
9364                    value: None,
9365                },
9366                Capability {
9367                    name: "server-option".into(),
9368                    value: None,
9369                },
9370                Capability {
9371                    name: "server-info".into(),
9372                    value: Some("custom".into()),
9373                },
9374            ],
9375        };
9376        assert_eq!(
9377            classify_protocol_v2_command_request(
9378                &handshake,
9379                ObjectFormat::Sha1,
9380                &ProtocolV2CommandRequest {
9381                    command: "ls-refs".into(),
9382                    capabilities: Vec::new(),
9383                    arguments: vec![b"unborn".to_vec()],
9384                },
9385            )
9386            .expect("test operation should succeed"),
9387            ProtocolV2Command::LsRefs(ProtocolV2LsRefsRequest {
9388                unborn: true,
9389                ..ProtocolV2LsRefsRequest::default()
9390            })
9391        );
9392        assert_eq!(
9393            classify_protocol_v2_command_request(
9394                &handshake,
9395                ObjectFormat::Sha1,
9396                &ProtocolV2CommandRequest {
9397                    command: "fetch".into(),
9398                    capabilities: Vec::new(),
9399                    arguments: vec![
9400                        b"want-ref refs/heads/main".to_vec(),
9401                        b"filter blob:none".to_vec(),
9402                    ],
9403                },
9404            )
9405            .expect("test operation should succeed"),
9406            ProtocolV2Command::Fetch(ProtocolV2FetchRequest {
9407                want_refs: vec!["refs/heads/main".into()],
9408                filter: Some("blob:none".into()),
9409                ..ProtocolV2FetchRequest::default()
9410            })
9411        );
9412        assert_eq!(
9413            classify_protocol_v2_command_request(
9414                &handshake,
9415                ObjectFormat::Sha1,
9416                &ProtocolV2CommandRequest {
9417                    command: "object-info".into(),
9418                    capabilities: Vec::new(),
9419                    arguments: vec![
9420                        b"size".to_vec(),
9421                        b"oid 1111111111111111111111111111111111111111".to_vec(),
9422                    ],
9423                },
9424            )
9425            .expect("test operation should succeed"),
9426            ProtocolV2Command::ObjectInfo(ProtocolV2ObjectInfoRequest {
9427                size: true,
9428                oids: vec![
9429                    ObjectId::from_hex(
9430                        ObjectFormat::Sha1,
9431                        "1111111111111111111111111111111111111111",
9432                    )
9433                    .expect("test operation should succeed")
9434                ],
9435            })
9436        );
9437
9438        let unknown = ProtocolV2CommandRequest {
9439            command: "server-info".into(),
9440            capabilities: vec![Capability {
9441                name: "server-option".into(),
9442                value: Some("trace=true".into()),
9443            }],
9444            arguments: Vec::new(),
9445        };
9446        assert_eq!(
9447            classify_protocol_v2_command_request(&handshake, ObjectFormat::Sha1, &unknown)
9448                .expect("test operation should succeed"),
9449            ProtocolV2Command::Unknown(unknown)
9450        );
9451        assert!(
9452            classify_protocol_v2_command_request(
9453                &handshake,
9454                ObjectFormat::Sha1,
9455                &ProtocolV2CommandRequest {
9456                    command: "not-advertised".into(),
9457                    capabilities: Vec::new(),
9458                    arguments: Vec::new(),
9459                },
9460            )
9461            .is_err()
9462        );
9463    }
9464
9465    #[test]
9466    fn protocol_v2_session_request_classifies_streamed_command_and_done() {
9467        let handshake = TransportHandshake {
9468            protocol: ProtocolVersion::V2,
9469            capabilities: vec![
9470                Capability {
9471                    name: "ls-refs".into(),
9472                    value: Some("unborn".into()),
9473                },
9474                Capability {
9475                    name: "fetch".into(),
9476                    value: Some("filter ref-in-want".into()),
9477                },
9478            ],
9479        };
9480        let command = ProtocolV2Request::Command(ProtocolV2CommandRequest {
9481            command: "ls-refs".into(),
9482            capabilities: Vec::new(),
9483            arguments: vec![b"unborn".to_vec()],
9484        });
9485        assert_eq!(
9486            classify_protocol_v2_request(&handshake, ObjectFormat::Sha1, &command)
9487                .expect("test operation should succeed"),
9488            ProtocolV2SessionRequest::Command(ProtocolV2Command::LsRefs(ProtocolV2LsRefsRequest {
9489                unborn: true,
9490                ..ProtocolV2LsRefsRequest::default()
9491            }))
9492        );
9493        assert_eq!(
9494            classify_protocol_v2_request(&handshake, ObjectFormat::Sha1, &ProtocolV2Request::Done)
9495                .expect("test operation should succeed"),
9496            ProtocolV2SessionRequest::Done
9497        );
9498
9499        let mut encoded = Vec::new();
9500        write_protocol_v2_request(&mut encoded, &command).expect("test operation should succeed");
9501        write_protocol_v2_request(&mut encoded, &ProtocolV2Request::Done)
9502            .expect("test operation should succeed");
9503        encoded.extend_from_slice(b"tail");
9504
9505        let mut input = encoded.as_slice();
9506        assert_eq!(
9507            read_protocol_v2_session_request(&handshake, ObjectFormat::Sha1, &mut input)
9508                .expect("test operation should succeed"),
9509            ProtocolV2SessionRequest::Command(ProtocolV2Command::LsRefs(ProtocolV2LsRefsRequest {
9510                unborn: true,
9511                ..ProtocolV2LsRefsRequest::default()
9512            }))
9513        );
9514        assert_eq!(
9515            read_protocol_v2_session_request(&handshake, ObjectFormat::Sha1, &mut input)
9516                .expect("test operation should succeed"),
9517            ProtocolV2SessionRequest::Done
9518        );
9519        assert_eq!(input, b"tail");
9520    }
9521
9522    #[test]
9523    fn advertised_ref_parses_first_v0_capability_line() {
9524        let payload =
9525            b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 HEAD\0multi_ack symref=HEAD:refs/heads/main\n";
9526        let advertisement = parse_ref_advertisement(ObjectFormat::Sha1, payload)
9527            .expect("test operation should succeed");
9528        assert_eq!(
9529            advertisement.oid,
9530            ObjectId::from_hex(
9531                ObjectFormat::Sha1,
9532                "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"
9533            )
9534            .expect("test operation should succeed")
9535        );
9536        assert_eq!(advertisement.name, "HEAD");
9537        assert_eq!(
9538            advertisement.capabilities,
9539            vec![
9540                Capability {
9541                    name: "multi_ack".into(),
9542                    value: None,
9543                },
9544                Capability {
9545                    name: "symref".into(),
9546                    value: Some("HEAD:refs/heads/main".into()),
9547                },
9548            ]
9549        );
9550    }
9551
9552    #[test]
9553    fn advertised_ref_parses_lines_without_capabilities() {
9554        let advertisement = parse_ref_advertisement(
9555            ObjectFormat::Sha1,
9556            b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n",
9557        )
9558        .expect("test operation should succeed");
9559        assert_eq!(advertisement.name, "refs/heads/main");
9560        assert!(advertisement.capabilities.is_empty());
9561    }
9562
9563    #[test]
9564    fn advertised_ref_rejects_malformed_payloads() {
9565        assert!(
9566            parse_ref_advertisement(ObjectFormat::Sha1, b"not-an-oid refs/heads/main\n").is_err()
9567        );
9568        assert!(
9569            parse_ref_advertisement(
9570                ObjectFormat::Sha1,
9571                b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n"
9572            )
9573            .is_err()
9574        );
9575    }
9576
9577    #[test]
9578    fn advertised_refs_parse_and_encode_stream() {
9579        let main = ObjectId::from_hex(
9580            ObjectFormat::Sha1,
9581            "1111111111111111111111111111111111111111",
9582        )
9583        .expect("test operation should succeed");
9584        let feature = ObjectId::from_hex(
9585            ObjectFormat::Sha1,
9586            "2222222222222222222222222222222222222222",
9587        )
9588        .expect("test operation should succeed");
9589        let frames = vec![
9590            PktLineFrame::Data(
9591                b"1111111111111111111111111111111111111111 HEAD\0multi_ack thin-pack agent=git/2.54.0\n"
9592                    .to_vec(),
9593            ),
9594            PktLineFrame::Data(
9595                b"2222222222222222222222222222222222222222 refs/heads/feature\n".to_vec(),
9596            ),
9597            PktLineFrame::Flush,
9598        ];
9599        let advertisements = parse_ref_advertisements(ObjectFormat::Sha1, &frames)
9600            .expect("test operation should succeed");
9601        assert_eq!(
9602            advertisements,
9603            vec![
9604                RefAdvertisement {
9605                    oid: main,
9606                    name: "HEAD".into(),
9607                    capabilities: vec![
9608                        Capability {
9609                            name: "multi_ack".into(),
9610                            value: None,
9611                        },
9612                        Capability {
9613                            name: "thin-pack".into(),
9614                            value: None,
9615                        },
9616                        Capability {
9617                            name: "agent".into(),
9618                            value: Some("git/2.54.0".into()),
9619                        },
9620                    ],
9621                },
9622                RefAdvertisement {
9623                    oid: feature,
9624                    name: "refs/heads/feature".into(),
9625                    capabilities: Vec::new(),
9626                },
9627            ]
9628        );
9629        assert_eq!(
9630            encode_ref_advertisements(&advertisements).expect("test operation should succeed"),
9631            frames
9632        );
9633        assert_eq!(
9634            parse_ref_advertisements(ObjectFormat::Sha1, &[PktLineFrame::Flush])
9635                .expect("test operation should succeed"),
9636            Vec::<RefAdvertisement>::new()
9637        );
9638    }
9639
9640    #[test]
9641    fn advertised_ref_set_parses_v1_version_refs_and_shallow() {
9642        let main = ObjectId::from_hex(
9643            ObjectFormat::Sha1,
9644            "1111111111111111111111111111111111111111",
9645        )
9646        .expect("test operation should succeed");
9647        let feature = ObjectId::from_hex(
9648            ObjectFormat::Sha1,
9649            "2222222222222222222222222222222222222222",
9650        )
9651        .expect("test operation should succeed");
9652        let shallow = ObjectId::from_hex(
9653            ObjectFormat::Sha1,
9654            "3333333333333333333333333333333333333333",
9655        )
9656        .expect("test operation should succeed");
9657        let frames = vec![
9658            PktLineFrame::Data(b"version 1\n".to_vec()),
9659            PktLineFrame::Data(
9660                b"1111111111111111111111111111111111111111 HEAD\0multi_ack symref=HEAD:refs/heads/main\n"
9661                    .to_vec(),
9662            ),
9663            PktLineFrame::Data(
9664                b"2222222222222222222222222222222222222222 refs/heads/feature\n".to_vec(),
9665            ),
9666            PktLineFrame::Data(b"shallow 3333333333333333333333333333333333333333\n".to_vec()),
9667            PktLineFrame::Flush,
9668        ];
9669
9670        let set = parse_ref_advertisement_set(ObjectFormat::Sha1, &frames)
9671            .expect("test operation should succeed");
9672        assert_eq!(set.protocol, ProtocolVersion::V1);
9673        assert_eq!(set.shallow, vec![shallow]);
9674        assert_eq!(
9675            set.refs,
9676            vec![
9677                RefAdvertisement {
9678                    oid: main,
9679                    name: "HEAD".into(),
9680                    capabilities: vec![
9681                        Capability {
9682                            name: "multi_ack".into(),
9683                            value: None,
9684                        },
9685                        Capability {
9686                            name: "symref".into(),
9687                            value: Some("HEAD:refs/heads/main".into()),
9688                        },
9689                    ],
9690                },
9691                RefAdvertisement {
9692                    oid: feature,
9693                    name: "refs/heads/feature".into(),
9694                    capabilities: Vec::new(),
9695                },
9696            ]
9697        );
9698        assert_eq!(
9699            parse_ref_advertisements(ObjectFormat::Sha1, &frames)
9700                .expect("test operation should succeed"),
9701            set.refs
9702        );
9703        assert_eq!(
9704            encode_ref_advertisement_set(&set).expect("test operation should succeed"),
9705            frames
9706        );
9707    }
9708
9709    #[test]
9710    fn advertised_refs_streams_round_trip() {
9711        let advertisements = vec![RefAdvertisement {
9712            oid: ObjectId::from_hex(
9713                ObjectFormat::Sha1,
9714                "1111111111111111111111111111111111111111",
9715            )
9716            .expect("test operation should succeed"),
9717            name: "HEAD".into(),
9718            capabilities: vec![Capability {
9719                name: "symref".into(),
9720                value: Some("HEAD:refs/heads/main".into()),
9721            }],
9722        }];
9723        let mut encoded = Vec::new();
9724        write_ref_advertisements(&mut encoded, &advertisements)
9725            .expect("test operation should succeed");
9726        encoded.extend_from_slice(b"tail");
9727
9728        let mut input = encoded.as_slice();
9729        assert_eq!(
9730            read_ref_advertisements(ObjectFormat::Sha1, &mut input)
9731                .expect("test operation should succeed"),
9732            advertisements
9733        );
9734        assert_eq!(input, b"tail");
9735    }
9736
9737    #[test]
9738    fn advertised_ref_set_streams_round_trip() {
9739        let set = RefAdvertisementSet {
9740            protocol: ProtocolVersion::V1,
9741            refs: vec![RefAdvertisement {
9742                oid: ObjectId::from_hex(
9743                    ObjectFormat::Sha1,
9744                    "1111111111111111111111111111111111111111",
9745                )
9746                .expect("test operation should succeed"),
9747                name: "HEAD".into(),
9748                capabilities: vec![Capability {
9749                    name: "symref".into(),
9750                    value: Some("HEAD:refs/heads/main".into()),
9751                }],
9752            }],
9753            shallow: vec![
9754                ObjectId::from_hex(
9755                    ObjectFormat::Sha1,
9756                    "2222222222222222222222222222222222222222",
9757                )
9758                .expect("test operation should succeed"),
9759            ],
9760        };
9761        let mut encoded = Vec::new();
9762        write_ref_advertisement_set(&mut encoded, &set).expect("test operation should succeed");
9763        encoded.extend_from_slice(b"tail");
9764
9765        let mut input = encoded.as_slice();
9766        assert_eq!(
9767            read_ref_advertisement_set(ObjectFormat::Sha1, &mut input)
9768                .expect("test operation should succeed"),
9769            set
9770        );
9771        assert_eq!(input, b"tail");
9772    }
9773
9774    #[test]
9775    fn advertised_refs_reject_malformed_streams() {
9776        assert!(
9777            parse_ref_advertisements(
9778                ObjectFormat::Sha1,
9779                &[PktLineFrame::Data(
9780                    b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),
9781                )],
9782            )
9783            .is_err()
9784        );
9785        assert!(
9786            parse_ref_advertisements(
9787                ObjectFormat::Sha1,
9788                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
9789            )
9790            .is_err()
9791        );
9792        assert!(parse_ref_advertisements(
9793            ObjectFormat::Sha1,
9794            &[
9795                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
9796                PktLineFrame::Data(
9797                    b"2222222222222222222222222222222222222222 refs/heads/main\0thin-pack\n"
9798                        .to_vec(),
9799                ),
9800                PktLineFrame::Flush,
9801            ],
9802        )
9803        .is_err());
9804        assert!(parse_ref_advertisement_set(
9805            ObjectFormat::Sha1,
9806            &[
9807                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
9808                PktLineFrame::Data(b"version 1\n".to_vec()),
9809                PktLineFrame::Flush,
9810            ],
9811        )
9812        .is_err());
9813        assert!(
9814            parse_ref_advertisement_set(
9815                ObjectFormat::Sha1,
9816                &[
9817                    PktLineFrame::Data(b"version 2\n".to_vec()),
9818                    PktLineFrame::Flush,
9819                ],
9820            )
9821            .is_err()
9822        );
9823        assert!(
9824            parse_ref_advertisement_set(
9825                ObjectFormat::Sha1,
9826                &[
9827                    PktLineFrame::Data(
9828                        b"shallow 1111111111111111111111111111111111111111\n".to_vec()
9829                    ),
9830                    PktLineFrame::Flush,
9831                ],
9832            )
9833            .is_err()
9834        );
9835        assert!(parse_ref_advertisement_set(
9836            ObjectFormat::Sha1,
9837            &[
9838                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
9839                PktLineFrame::Data(b"shallow not-an-oid\n".to_vec()),
9840                PktLineFrame::Flush,
9841            ],
9842        )
9843        .is_err());
9844        assert!(parse_ref_advertisement_set(
9845            ObjectFormat::Sha1,
9846            &[
9847                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
9848                PktLineFrame::Data(b"shallow 2222222222222222222222222222222222222222\n".to_vec()),
9849                PktLineFrame::Data(
9850                    b"3333333333333333333333333333333333333333 refs/heads/main\n".to_vec(),
9851                ),
9852                PktLineFrame::Flush,
9853            ],
9854        )
9855        .is_err());
9856        assert!(
9857            encode_ref_advertisements(&[
9858                RefAdvertisement {
9859                    oid: ObjectId::from_hex(
9860                        ObjectFormat::Sha1,
9861                        "1111111111111111111111111111111111111111",
9862                    )
9863                    .expect("test operation should succeed"),
9864                    name: "HEAD".into(),
9865                    capabilities: Vec::new(),
9866                },
9867                RefAdvertisement {
9868                    oid: ObjectId::from_hex(
9869                        ObjectFormat::Sha1,
9870                        "2222222222222222222222222222222222222222",
9871                    )
9872                    .expect("test operation should succeed"),
9873                    name: "refs/heads/main".into(),
9874                    capabilities: vec![Capability {
9875                        name: "thin-pack".into(),
9876                        value: None,
9877                    }],
9878                },
9879            ])
9880            .is_err()
9881        );
9882        assert!(
9883            encode_ref_advertisement(&RefAdvertisement {
9884                oid: ObjectId::from_hex(
9885                    ObjectFormat::Sha1,
9886                    "1111111111111111111111111111111111111111",
9887                )
9888                .expect("test operation should succeed"),
9889                name: "bad ref".into(),
9890                capabilities: Vec::new(),
9891            })
9892            .is_err()
9893        );
9894        assert!(
9895            encode_ref_advertisement_set(&RefAdvertisementSet {
9896                protocol: ProtocolVersion::V2,
9897                refs: Vec::new(),
9898                shallow: Vec::new(),
9899            })
9900            .is_err()
9901        );
9902        assert!(
9903            encode_ref_advertisement_set(&RefAdvertisementSet {
9904                protocol: ProtocolVersion::V0,
9905                refs: Vec::new(),
9906                shallow: vec![
9907                    ObjectId::from_hex(
9908                        ObjectFormat::Sha1,
9909                        "1111111111111111111111111111111111111111",
9910                    )
9911                    .expect("test operation should succeed")
9912                ],
9913            })
9914            .is_err()
9915        );
9916    }
9917
9918    #[test]
9919    fn dumb_http_info_refs_parse_and_encode_records() {
9920        let main = ObjectId::from_hex(
9921            ObjectFormat::Sha1,
9922            "1111111111111111111111111111111111111111",
9923        )
9924        .expect("test operation should succeed");
9925        let tag = ObjectId::from_hex(
9926            ObjectFormat::Sha1,
9927            "2222222222222222222222222222222222222222",
9928        )
9929        .expect("test operation should succeed");
9930        let peeled = ObjectId::from_hex(
9931            ObjectFormat::Sha1,
9932            "3333333333333333333333333333333333333333",
9933        )
9934        .expect("test operation should succeed");
9935        let input = b"1111111111111111111111111111111111111111\trefs/heads/main\n2222222222222222222222222222222222222222\trefs/tags/v1.0\n3333333333333333333333333333333333333333\trefs/tags/v1.0^{}\n";
9936
9937        let records = parse_dumb_http_info_refs(ObjectFormat::Sha1, input)
9938            .expect("test operation should succeed");
9939        assert_eq!(
9940            records,
9941            vec![
9942                DumbHttpRefRecord {
9943                    oid: main,
9944                    name: "refs/heads/main".into(),
9945                    peeled: false,
9946                },
9947                DumbHttpRefRecord {
9948                    oid: tag,
9949                    name: "refs/tags/v1.0".into(),
9950                    peeled: false,
9951                },
9952                DumbHttpRefRecord {
9953                    oid: peeled,
9954                    name: "refs/tags/v1.0".into(),
9955                    peeled: true,
9956                },
9957            ]
9958        );
9959        assert_eq!(
9960            encode_dumb_http_info_refs(&records).expect("test operation should succeed"),
9961            input
9962        );
9963        assert_eq!(
9964            parse_dumb_http_info_refs(ObjectFormat::Sha1, b"")
9965                .expect("test operation should succeed"),
9966            Vec::<DumbHttpRefRecord>::new()
9967        );
9968    }
9969
9970    #[test]
9971    fn dumb_http_info_refs_streams_round_trip() {
9972        let records = vec![DumbHttpRefRecord {
9973            oid: ObjectId::from_hex(
9974                ObjectFormat::Sha1,
9975                "1111111111111111111111111111111111111111",
9976            )
9977            .expect("test operation should succeed"),
9978            name: "refs/heads/main".into(),
9979            peeled: false,
9980        }];
9981        let mut encoded = Vec::new();
9982        write_dumb_http_info_refs(&mut encoded, &records).expect("test operation should succeed");
9983        let mut input = encoded.as_slice();
9984        assert_eq!(
9985            read_dumb_http_info_refs(ObjectFormat::Sha1, &mut input)
9986                .expect("test operation should succeed"),
9987            records
9988        );
9989        assert!(input.is_empty());
9990    }
9991
9992    #[test]
9993    fn dumb_http_info_refs_reject_malformed_records() {
9994        assert!(
9995            parse_dumb_http_info_refs(
9996                ObjectFormat::Sha1,
9997                b"1111111111111111111111111111111111111111 refs/heads/main\n",
9998            )
9999            .is_err()
10000        );
10001        assert!(
10002            parse_dumb_http_info_refs(
10003                ObjectFormat::Sha1,
10004                b"1111111111111111111111111111111111111111\trefs/heads/main",
10005            )
10006            .is_err()
10007        );
10008        assert!(
10009            parse_dumb_http_info_refs(ObjectFormat::Sha1, b"not-an-oid\trefs/heads/main\n")
10010                .is_err()
10011        );
10012        assert!(
10013            parse_dumb_http_info_refs(
10014                ObjectFormat::Sha1,
10015                b"1111111111111111111111111111111111111111\tbad ref\n",
10016            )
10017            .is_err()
10018        );
10019        assert!(
10020            encode_dumb_http_info_refs(&[DumbHttpRefRecord {
10021                oid: ObjectId::from_hex(
10022                    ObjectFormat::Sha1,
10023                    "1111111111111111111111111111111111111111",
10024                )
10025                .expect("test operation should succeed"),
10026                name: "refs/tags/v1.0^{}".into(),
10027                peeled: false,
10028            }])
10029            .is_err()
10030        );
10031    }
10032
10033    #[test]
10034    fn dumb_http_alternates_parse_and_encode_locations() {
10035        let input = b"https://example.com/base.git/objects/\n../other.git/objects/\n";
10036        let alternates = parse_dumb_http_alternates(input).expect("test operation should succeed");
10037        assert_eq!(
10038            alternates,
10039            vec![
10040                "https://example.com/base.git/objects/".to_string(),
10041                "../other.git/objects/".to_string(),
10042            ]
10043        );
10044        assert_eq!(
10045            encode_dumb_http_alternates(&alternates).expect("test operation should succeed"),
10046            input
10047        );
10048        assert_eq!(
10049            parse_dumb_http_alternates(b"").expect("test operation should succeed"),
10050            Vec::<String>::new()
10051        );
10052    }
10053
10054    #[test]
10055    fn dumb_http_alternates_streams_round_trip() {
10056        let alternates = vec!["https://example.com/base.git/objects/".to_string()];
10057        let mut encoded = Vec::new();
10058        write_dumb_http_alternates(&mut encoded, &alternates)
10059            .expect("test operation should succeed");
10060        let mut input = encoded.as_slice();
10061        assert_eq!(
10062            read_dumb_http_alternates(&mut input).expect("test operation should succeed"),
10063            alternates
10064        );
10065        assert!(input.is_empty());
10066    }
10067
10068    #[test]
10069    fn dumb_http_alternates_reject_malformed_lines() {
10070        assert!(parse_dumb_http_alternates(b"https://example.com/base.git/objects/").is_err());
10071        assert!(parse_dumb_http_alternates(b"\n").is_err());
10072        assert!(parse_dumb_http_alternates(b"https://example.com/base.git/objects/\r\n").is_err());
10073        assert!(encode_dumb_http_alternates(&["bad\nalternate".to_string()]).is_err());
10074    }
10075
10076    #[test]
10077    fn dumb_http_packs_parse_and_encode_pack_records() {
10078        let first = ObjectId::from_hex(
10079            ObjectFormat::Sha1,
10080            "1111111111111111111111111111111111111111",
10081        )
10082        .expect("test operation should succeed");
10083        let second = ObjectId::from_hex(
10084            ObjectFormat::Sha1,
10085            "2222222222222222222222222222222222222222",
10086        )
10087        .expect("test operation should succeed");
10088        let input = b"P pack-1111111111111111111111111111111111111111.pack\nP pack-2222222222222222222222222222222222222222.pack\n";
10089        let records = parse_dumb_http_packs(ObjectFormat::Sha1, input)
10090            .expect("test operation should succeed");
10091        assert_eq!(
10092            records,
10093            vec![
10094                DumbHttpPackRecord { hash: first },
10095                DumbHttpPackRecord { hash: second },
10096            ]
10097        );
10098        assert_eq!(
10099            encode_dumb_http_packs(&records).expect("test operation should succeed"),
10100            input
10101        );
10102        assert_eq!(
10103            parse_dumb_http_packs(ObjectFormat::Sha1, b"").expect("test operation should succeed"),
10104            Vec::<DumbHttpPackRecord>::new()
10105        );
10106    }
10107
10108    #[test]
10109    fn dumb_http_packs_streams_round_trip() {
10110        let records = vec![DumbHttpPackRecord {
10111            hash: ObjectId::from_hex(
10112                ObjectFormat::Sha1,
10113                "1111111111111111111111111111111111111111",
10114            )
10115            .expect("test operation should succeed"),
10116        }];
10117        let mut encoded = Vec::new();
10118        write_dumb_http_packs(&mut encoded, &records).expect("test operation should succeed");
10119        let mut input = encoded.as_slice();
10120        assert_eq!(
10121            read_dumb_http_packs(ObjectFormat::Sha1, &mut input)
10122                .expect("test operation should succeed"),
10123            records
10124        );
10125        assert!(input.is_empty());
10126    }
10127
10128    #[test]
10129    fn dumb_http_packs_reject_malformed_records() {
10130        assert!(
10131            parse_dumb_http_packs(
10132                ObjectFormat::Sha1,
10133                b"P pack-1111111111111111111111111111111111111111.pack",
10134            )
10135            .is_err()
10136        );
10137        assert!(
10138            parse_dumb_http_packs(
10139                ObjectFormat::Sha1,
10140                b"pack-1111111111111111111111111111111111111111.pack\n",
10141            )
10142            .is_err()
10143        );
10144        assert!(parse_dumb_http_packs(ObjectFormat::Sha1, b"P pack-not-a-hash.pack\n",).is_err());
10145        assert!(
10146            parse_dumb_http_packs(
10147                ObjectFormat::Sha1,
10148                b"P pack-1111111111111111111111111111111111111111.idx\n",
10149            )
10150            .is_err()
10151        );
10152    }
10153
10154    #[test]
10155    fn upload_pack_features_parse_encode_and_validate_request() {
10156        let capabilities = vec![
10157            Capability {
10158                name: "multi_ack".into(),
10159                value: None,
10160            },
10161            Capability {
10162                name: "multi_ack_detailed".into(),
10163                value: None,
10164            },
10165            Capability {
10166                name: "no-done".into(),
10167                value: None,
10168            },
10169            Capability {
10170                name: "thin-pack".into(),
10171                value: None,
10172            },
10173            Capability {
10174                name: "side-band-64k".into(),
10175                value: None,
10176            },
10177            Capability {
10178                name: "ofs-delta".into(),
10179                value: None,
10180            },
10181            Capability {
10182                name: "shallow".into(),
10183                value: None,
10184            },
10185            Capability {
10186                name: "deepen-since".into(),
10187                value: None,
10188            },
10189            Capability {
10190                name: "deepen-not".into(),
10191                value: None,
10192            },
10193            Capability {
10194                name: "include-tag".into(),
10195                value: None,
10196            },
10197            Capability {
10198                name: "no-progress".into(),
10199                value: None,
10200            },
10201            Capability {
10202                name: "filter".into(),
10203                value: None,
10204            },
10205            Capability {
10206                name: "agent".into(),
10207                value: Some("git/2.54.0".into()),
10208            },
10209            Capability {
10210                name: "object-format".into(),
10211                value: Some("sha256".into()),
10212            },
10213            Capability {
10214                name: "symref".into(),
10215                value: Some("HEAD:refs/heads/main".into()),
10216            },
10217            Capability {
10218                name: "custom".into(),
10219                value: Some("value".into()),
10220            },
10221        ];
10222        let features =
10223            parse_upload_pack_features(&capabilities).expect("test operation should succeed");
10224        assert_eq!(
10225            features,
10226            UploadPackFeatures {
10227                multi_ack: true,
10228                multi_ack_detailed: true,
10229                no_done: true,
10230                thin_pack: true,
10231                side_band_64k: true,
10232                ofs_delta: true,
10233                shallow: true,
10234                deepen_since: true,
10235                deepen_not: true,
10236                include_tag: true,
10237                no_progress: true,
10238                filter: true,
10239                agent: Some("git/2.54.0".into()),
10240                object_format: Some(ObjectFormat::Sha256),
10241                symrefs: vec!["HEAD:refs/heads/main".into()],
10242                unknown: vec![Capability {
10243                    name: "custom".into(),
10244                    value: Some("value".into()),
10245                }],
10246                ..UploadPackFeatures::default()
10247            }
10248        );
10249        assert_eq!(
10250            encode_upload_pack_features(&features).expect("test operation should succeed"),
10251            capabilities
10252        );
10253
10254        let request = UploadPackRequest {
10255            wants: vec![
10256                ObjectId::from_hex(
10257                    ObjectFormat::Sha1,
10258                    "1111111111111111111111111111111111111111",
10259                )
10260                .expect("test operation should succeed"),
10261            ],
10262            capabilities: vec![
10263                Capability {
10264                    name: "multi_ack_detailed".into(),
10265                    value: None,
10266                },
10267                Capability {
10268                    name: "thin-pack".into(),
10269                    value: None,
10270                },
10271                Capability {
10272                    name: "side-band-64k".into(),
10273                    value: None,
10274                },
10275                Capability {
10276                    name: "ofs-delta".into(),
10277                    value: None,
10278                },
10279                Capability {
10280                    name: "include-tag".into(),
10281                    value: None,
10282                },
10283                Capability {
10284                    name: "agent".into(),
10285                    value: Some("sley".into()),
10286                },
10287            ],
10288            shallow: vec![
10289                ObjectId::from_hex(
10290                    ObjectFormat::Sha1,
10291                    "2222222222222222222222222222222222222222",
10292                )
10293                .expect("test operation should succeed"),
10294            ],
10295            deepen: Some(5),
10296            deepen_since: Some(1_710_000_000),
10297            deepen_not: vec!["refs/tags/base".into()],
10298            filter: Some("blob:none".into()),
10299        };
10300        validate_upload_pack_request_features(&features, &request)
10301            .expect("test operation should succeed");
10302    }
10303
10304    #[test]
10305    fn upload_pack_features_reject_invalid_requests() {
10306        let want = ObjectId::from_hex(
10307            ObjectFormat::Sha1,
10308            "1111111111111111111111111111111111111111",
10309        )
10310        .expect("test operation should succeed");
10311        let features = UploadPackFeatures {
10312            thin_pack: true,
10313            side_band: true,
10314            ..UploadPackFeatures::default()
10315        };
10316
10317        assert!(
10318            validate_upload_pack_request_features(
10319                &features,
10320                &UploadPackRequest {
10321                    wants: vec![want],
10322                    capabilities: vec![Capability {
10323                        name: "ofs-delta".into(),
10324                        value: None,
10325                    }],
10326                    ..UploadPackRequest::default()
10327                },
10328            )
10329            .is_err()
10330        );
10331        assert!(
10332            validate_upload_pack_request_features(
10333                &features,
10334                &UploadPackRequest {
10335                    wants: vec![want],
10336                    shallow: vec![want],
10337                    ..UploadPackRequest::default()
10338                },
10339            )
10340            .is_err()
10341        );
10342        assert!(
10343            validate_upload_pack_request_features(
10344                &features,
10345                &UploadPackRequest {
10346                    wants: vec![want],
10347                    filter: Some("blob:none".into()),
10348                    ..UploadPackRequest::default()
10349                },
10350            )
10351            .is_err()
10352        );
10353        assert!(
10354            validate_upload_pack_request_features(
10355                &UploadPackFeatures {
10356                    side_band: true,
10357                    side_band_64k: true,
10358                    ..UploadPackFeatures::default()
10359                },
10360                &UploadPackRequest {
10361                    wants: vec![want],
10362                    capabilities: vec![
10363                        Capability {
10364                            name: "side-band".into(),
10365                            value: None,
10366                        },
10367                        Capability {
10368                            name: "side-band-64k".into(),
10369                            value: None,
10370                        },
10371                    ],
10372                    ..UploadPackRequest::default()
10373                },
10374            )
10375            .is_err()
10376        );
10377
10378        assert!(
10379            parse_upload_pack_features(&[
10380                Capability {
10381                    name: "thin-pack".into(),
10382                    value: None,
10383                },
10384                Capability {
10385                    name: "thin-pack".into(),
10386                    value: None,
10387                },
10388            ])
10389            .is_err()
10390        );
10391        assert!(
10392            encode_upload_pack_features(&UploadPackFeatures {
10393                unknown: vec![Capability {
10394                    name: "filter".into(),
10395                    value: None,
10396                }],
10397                ..UploadPackFeatures::default()
10398            })
10399            .is_err()
10400        );
10401    }
10402
10403    #[test]
10404    fn upload_pack_raw_response_builder_filters_unknown_haves_and_builds_pack() {
10405        let want = ObjectId::from_hex(
10406            ObjectFormat::Sha1,
10407            "1111111111111111111111111111111111111111",
10408        )
10409        .expect("test operation should succeed");
10410        let known_have = ObjectId::from_hex(
10411            ObjectFormat::Sha1,
10412            "2222222222222222222222222222222222222222",
10413        )
10414        .expect("test operation should succeed");
10415        let unknown_have = ObjectId::from_hex(
10416            ObjectFormat::Sha1,
10417            "3333333333333333333333333333333333333333",
10418        )
10419        .expect("test operation should succeed");
10420        let existing = std::collections::HashSet::from([want, known_have]);
10421
10422        let response = build_upload_pack_raw_packfile_response(
10423            &UploadPackFeatures::default(),
10424            UploadPackRequest {
10425                wants: vec![want],
10426                ..UploadPackRequest::default()
10427            },
10428            [known_have, unknown_have],
10429            |oid| Ok(existing.contains(oid)),
10430            |wants, haves| {
10431                assert_eq!(wants, vec![want]);
10432                assert_eq!(haves, vec![known_have]);
10433                Ok(Some(b"PACKmock".to_vec()))
10434            },
10435        )
10436        .expect("test operation should succeed");
10437
10438        assert_eq!(
10439            response.acknowledgments,
10440            vec![UploadPackAcknowledgment::Nak]
10441        );
10442        assert_eq!(response.packfile, b"PACKmock");
10443    }
10444
10445    #[test]
10446    fn upload_pack_raw_response_builder_rejects_missing_want_and_empty_pack() {
10447        let want = ObjectId::from_hex(
10448            ObjectFormat::Sha1,
10449            "1111111111111111111111111111111111111111",
10450        )
10451        .expect("test operation should succeed");
10452
10453        assert!(
10454            build_upload_pack_raw_packfile_response(
10455                &UploadPackFeatures::default(),
10456                UploadPackRequest {
10457                    wants: vec![want],
10458                    ..UploadPackRequest::default()
10459                },
10460                Vec::<ObjectId>::new(),
10461                |_| Ok(false),
10462                |_, _| Ok(Some(b"PACKmock".to_vec())),
10463            )
10464            .is_err()
10465        );
10466
10467        assert!(
10468            build_upload_pack_raw_packfile_response(
10469                &UploadPackFeatures::default(),
10470                UploadPackRequest {
10471                    wants: vec![want],
10472                    ..UploadPackRequest::default()
10473                },
10474                Vec::<ObjectId>::new(),
10475                |_| Ok(true),
10476                |_, _| Ok(None),
10477            )
10478            .is_err()
10479        );
10480    }
10481
10482    #[test]
10483    fn upload_pack_request_parses_and_encodes_initial_fetch_request() {
10484        let want = ObjectId::from_hex(
10485            ObjectFormat::Sha1,
10486            "1111111111111111111111111111111111111111",
10487        )
10488        .expect("test operation should succeed");
10489        let second_want = ObjectId::from_hex(
10490            ObjectFormat::Sha1,
10491            "2222222222222222222222222222222222222222",
10492        )
10493        .expect("test operation should succeed");
10494        let shallow = ObjectId::from_hex(
10495            ObjectFormat::Sha1,
10496            "3333333333333333333333333333333333333333",
10497        )
10498        .expect("test operation should succeed");
10499        let frames = vec![
10500            PktLineFrame::Data(
10501                b"want 1111111111111111111111111111111111111111 multi_ack thin-pack agent=git/2.54.0\n"
10502                    .to_vec(),
10503            ),
10504            PktLineFrame::Data(b"want 2222222222222222222222222222222222222222\n".to_vec()),
10505            PktLineFrame::Data(b"shallow 3333333333333333333333333333333333333333\n".to_vec()),
10506            PktLineFrame::Data(b"deepen-since 1710000000\n".to_vec()),
10507            PktLineFrame::Data(b"deepen-not refs/tags/base\n".to_vec()),
10508            PktLineFrame::Data(b"filter blob:none\n".to_vec()),
10509            PktLineFrame::Flush,
10510        ];
10511        let request = parse_upload_pack_request(ObjectFormat::Sha1, &frames)
10512            .expect("test operation should succeed")
10513            .expect("test operation should succeed");
10514        assert_eq!(
10515            request,
10516            UploadPackRequest {
10517                wants: vec![want, second_want],
10518                capabilities: vec![
10519                    Capability {
10520                        name: "multi_ack".into(),
10521                        value: None,
10522                    },
10523                    Capability {
10524                        name: "thin-pack".into(),
10525                        value: None,
10526                    },
10527                    Capability {
10528                        name: "agent".into(),
10529                        value: Some("git/2.54.0".into()),
10530                    },
10531                ],
10532                shallow: vec![shallow],
10533                deepen: None,
10534                deepen_since: Some(1_710_000_000),
10535                deepen_not: vec!["refs/tags/base".into()],
10536                filter: Some("blob:none".into()),
10537            }
10538        );
10539        assert_eq!(
10540            encode_upload_pack_request(Some(&request)).expect("test operation should succeed"),
10541            frames
10542        );
10543        assert_eq!(
10544            parse_upload_pack_request(ObjectFormat::Sha1, &[PktLineFrame::Flush])
10545                .expect("test operation should succeed"),
10546            None
10547        );
10548        assert_eq!(
10549            encode_upload_pack_request(None).expect("test operation should succeed"),
10550            vec![PktLineFrame::Flush]
10551        );
10552    }
10553
10554    #[test]
10555    fn upload_pack_request_streams_round_trip() {
10556        let request = UploadPackRequest {
10557            wants: vec![
10558                ObjectId::from_hex(
10559                    ObjectFormat::Sha1,
10560                    "1111111111111111111111111111111111111111",
10561                )
10562                .expect("test operation should succeed"),
10563            ],
10564            capabilities: vec![Capability {
10565                name: "ofs-delta".into(),
10566                value: None,
10567            }],
10568            deepen: Some(10),
10569            ..UploadPackRequest::default()
10570        };
10571        let mut encoded = Vec::new();
10572        write_upload_pack_request(&mut encoded, Some(&request))
10573            .expect("test operation should succeed");
10574        encoded.extend_from_slice(b"tail");
10575
10576        let mut input = encoded.as_slice();
10577        assert_eq!(
10578            read_upload_pack_request(ObjectFormat::Sha1, &mut input)
10579                .expect("test operation should succeed"),
10580            Some(request)
10581        );
10582        assert_eq!(input, b"tail");
10583    }
10584
10585    #[test]
10586    fn upload_pack_request_rejects_malformed_requests() {
10587        assert!(
10588            parse_upload_pack_request(
10589                ObjectFormat::Sha1,
10590                &[PktLineFrame::Data(
10591                    b"want 1111111111111111111111111111111111111111\n".to_vec(),
10592                )],
10593            )
10594            .is_err()
10595        );
10596        assert!(
10597            parse_upload_pack_request(
10598                ObjectFormat::Sha1,
10599                &[
10600                    PktLineFrame::Data(
10601                        b"shallow 1111111111111111111111111111111111111111\n".to_vec(),
10602                    ),
10603                    PktLineFrame::Flush,
10604                ],
10605            )
10606            .is_err()
10607        );
10608        assert!(
10609            parse_upload_pack_request(
10610                ObjectFormat::Sha1,
10611                &[
10612                    PktLineFrame::Data(
10613                        b"want 1111111111111111111111111111111111111111 thin-pack\n".to_vec(),
10614                    ),
10615                    PktLineFrame::Data(
10616                        b"want 2222222222222222222222222222222222222222 ofs-delta\n".to_vec(),
10617                    ),
10618                    PktLineFrame::Flush,
10619                ],
10620            )
10621            .is_err()
10622        );
10623        assert!(parse_upload_pack_request(
10624            ObjectFormat::Sha1,
10625            &[
10626                PktLineFrame::Data(b"want 1111111111111111111111111111111111111111\n".to_vec(),),
10627                PktLineFrame::Data(b"deepen 1\n".to_vec()),
10628                PktLineFrame::Data(b"want 2222222222222222222222222222222222222222\n".to_vec()),
10629                PktLineFrame::Flush,
10630            ],
10631        )
10632        .is_err());
10633        assert!(parse_upload_pack_request(
10634            ObjectFormat::Sha1,
10635            &[
10636                PktLineFrame::Data(b"want 1111111111111111111111111111111111111111\n".to_vec(),),
10637                PktLineFrame::Data(b"filter blob:none\n".to_vec()),
10638                PktLineFrame::Data(b"filter tree:0\n".to_vec()),
10639                PktLineFrame::Flush,
10640            ],
10641        )
10642        .is_err());
10643        assert!(encode_upload_pack_request(Some(&UploadPackRequest::default())).is_err());
10644        assert!(
10645            encode_upload_pack_request(Some(&UploadPackRequest {
10646                wants: vec![
10647                    ObjectId::from_hex(
10648                        ObjectFormat::Sha1,
10649                        "1111111111111111111111111111111111111111",
10650                    )
10651                    .expect("test operation should succeed")
10652                ],
10653                deepen: Some(0),
10654                ..UploadPackRequest::default()
10655            }))
10656            .is_err()
10657        );
10658    }
10659
10660    #[test]
10661    fn upload_pack_shallow_update_parses_and_encodes_records() {
10662        let shallow = ObjectId::from_hex(
10663            ObjectFormat::Sha1,
10664            "1111111111111111111111111111111111111111",
10665        )
10666        .expect("test operation should succeed");
10667        let unshallow = ObjectId::from_hex(
10668            ObjectFormat::Sha1,
10669            "2222222222222222222222222222222222222222",
10670        )
10671        .expect("test operation should succeed");
10672        let frames = vec![
10673            PktLineFrame::Data(b"shallow 1111111111111111111111111111111111111111\n".to_vec()),
10674            PktLineFrame::Data(b"unshallow 2222222222222222222222222222222222222222\n".to_vec()),
10675            PktLineFrame::Flush,
10676        ];
10677        let entries = parse_upload_pack_shallow_update(ObjectFormat::Sha1, &frames)
10678            .expect("test operation should succeed");
10679        assert_eq!(
10680            entries,
10681            vec![
10682                ProtocolV2FetchShallowInfo::Shallow(shallow),
10683                ProtocolV2FetchShallowInfo::Unshallow(unshallow),
10684            ]
10685        );
10686        assert_eq!(
10687            encode_upload_pack_shallow_update(&entries).expect("test operation should succeed"),
10688            frames
10689        );
10690        assert_eq!(
10691            parse_upload_pack_shallow_update(ObjectFormat::Sha1, &[PktLineFrame::Flush])
10692                .expect("test operation should succeed"),
10693            Vec::<ProtocolV2FetchShallowInfo>::new()
10694        );
10695    }
10696
10697    #[test]
10698    fn upload_pack_shallow_update_streams_round_trip() {
10699        let entries = vec![ProtocolV2FetchShallowInfo::Shallow(
10700            ObjectId::from_hex(
10701                ObjectFormat::Sha1,
10702                "1111111111111111111111111111111111111111",
10703            )
10704            .expect("test operation should succeed"),
10705        )];
10706        let mut encoded = Vec::new();
10707        write_upload_pack_shallow_update(&mut encoded, &entries)
10708            .expect("test operation should succeed");
10709        encoded.extend_from_slice(b"tail");
10710
10711        let mut input = encoded.as_slice();
10712        assert_eq!(
10713            read_upload_pack_shallow_update(ObjectFormat::Sha1, &mut input)
10714                .expect("test operation should succeed"),
10715            entries
10716        );
10717        assert_eq!(input, b"tail");
10718    }
10719
10720    #[test]
10721    fn upload_pack_shallow_update_rejects_malformed_records() {
10722        assert!(
10723            parse_upload_pack_shallow_update(
10724                ObjectFormat::Sha1,
10725                &[PktLineFrame::Data(
10726                    b"shallow 1111111111111111111111111111111111111111\n".to_vec(),
10727                )],
10728            )
10729            .is_err()
10730        );
10731        assert!(
10732            parse_upload_pack_shallow_update(
10733                ObjectFormat::Sha1,
10734                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
10735            )
10736            .is_err()
10737        );
10738        assert!(
10739            parse_upload_pack_shallow_update(
10740                ObjectFormat::Sha1,
10741                &[
10742                    PktLineFrame::Data(
10743                        b"shallow 1111111111111111111111111111111111111111\n".to_vec(),
10744                    ),
10745                    PktLineFrame::Flush,
10746                    PktLineFrame::Data(
10747                        b"unshallow 2222222222222222222222222222222222222222\n".to_vec(),
10748                    ),
10749                ],
10750            )
10751            .is_err()
10752        );
10753        assert!(
10754            parse_upload_pack_shallow_update(
10755                ObjectFormat::Sha1,
10756                &[
10757                    PktLineFrame::Data(
10758                        b"unsupported 1111111111111111111111111111111111111111\n".to_vec(),
10759                    ),
10760                    PktLineFrame::Flush,
10761                ],
10762            )
10763            .is_err()
10764        );
10765    }
10766
10767    #[test]
10768    fn upload_pack_negotiation_request_parses_flush_and_done_rounds() {
10769        let have = ObjectId::from_hex(
10770            ObjectFormat::Sha1,
10771            "1111111111111111111111111111111111111111",
10772        )
10773        .expect("test operation should succeed");
10774        let second_have = ObjectId::from_hex(
10775            ObjectFormat::Sha1,
10776            "2222222222222222222222222222222222222222",
10777        )
10778        .expect("test operation should succeed");
10779        let flush_round = vec![
10780            PktLineFrame::Data(b"have 1111111111111111111111111111111111111111\n".to_vec()),
10781            PktLineFrame::Data(b"have 2222222222222222222222222222222222222222\n".to_vec()),
10782            PktLineFrame::Flush,
10783        ];
10784        let request = parse_upload_pack_negotiation_request(ObjectFormat::Sha1, &flush_round)
10785            .expect("test operation should succeed");
10786        assert_eq!(
10787            request,
10788            UploadPackNegotiationRequest {
10789                haves: vec![have, second_have],
10790                done: false,
10791            }
10792        );
10793        assert_eq!(
10794            encode_upload_pack_negotiation_request(&request)
10795                .expect("test operation should succeed"),
10796            flush_round
10797        );
10798
10799        let done_round = vec![
10800            PktLineFrame::Data(b"have 1111111111111111111111111111111111111111\n".to_vec()),
10801            PktLineFrame::Data(b"done\n".to_vec()),
10802        ];
10803        let request = parse_upload_pack_negotiation_request(ObjectFormat::Sha1, &done_round)
10804            .expect("test operation should succeed");
10805        assert_eq!(
10806            request,
10807            UploadPackNegotiationRequest {
10808                haves: vec![have],
10809                done: true,
10810            }
10811        );
10812        assert_eq!(
10813            encode_upload_pack_negotiation_request(&request)
10814                .expect("test operation should succeed"),
10815            done_round
10816        );
10817    }
10818
10819    #[test]
10820    fn upload_pack_negotiation_request_streams_round_trip() {
10821        let first = UploadPackNegotiationRequest {
10822            haves: vec![
10823                ObjectId::from_hex(
10824                    ObjectFormat::Sha1,
10825                    "1111111111111111111111111111111111111111",
10826                )
10827                .expect("test operation should succeed"),
10828            ],
10829            done: false,
10830        };
10831        let second = UploadPackNegotiationRequest {
10832            haves: Vec::new(),
10833            done: true,
10834        };
10835        let mut encoded = Vec::new();
10836        write_upload_pack_negotiation_request(&mut encoded, &first)
10837            .expect("test operation should succeed");
10838        write_upload_pack_negotiation_request(&mut encoded, &second)
10839            .expect("test operation should succeed");
10840        encoded.extend_from_slice(b"tail");
10841
10842        let mut input = encoded.as_slice();
10843        assert_eq!(
10844            read_upload_pack_negotiation_request(ObjectFormat::Sha1, &mut input)
10845                .expect("test operation should succeed"),
10846            first
10847        );
10848        assert_eq!(
10849            read_upload_pack_negotiation_request(ObjectFormat::Sha1, &mut input)
10850                .expect("test operation should succeed"),
10851            second
10852        );
10853        assert_eq!(input, b"tail");
10854    }
10855
10856    #[test]
10857    fn upload_pack_negotiation_request_rejects_malformed_rounds() {
10858        assert!(
10859            parse_upload_pack_negotiation_request(
10860                ObjectFormat::Sha1,
10861                &[PktLineFrame::Data(
10862                    b"have 1111111111111111111111111111111111111111\n".to_vec(),
10863                )],
10864            )
10865            .is_err()
10866        );
10867        assert!(
10868            parse_upload_pack_negotiation_request(
10869                ObjectFormat::Sha1,
10870                &[PktLineFrame::Data(
10871                    b"want 1111111111111111111111111111111111111111\n".to_vec(),
10872                )],
10873            )
10874            .is_err()
10875        );
10876        assert!(parse_upload_pack_negotiation_request(
10877            ObjectFormat::Sha1,
10878            &[
10879                PktLineFrame::Data(b"done\n".to_vec()),
10880                PktLineFrame::Data(b"have 1111111111111111111111111111111111111111\n".to_vec(),),
10881            ],
10882        )
10883        .is_err());
10884        assert!(
10885            parse_upload_pack_negotiation_request(
10886                ObjectFormat::Sha1,
10887                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
10888            )
10889            .is_err()
10890        );
10891    }
10892
10893    #[test]
10894    fn upload_pack_acknowledgments_parse_and_encode_statuses() {
10895        let oid = ObjectId::from_hex(
10896            ObjectFormat::Sha1,
10897            "1111111111111111111111111111111111111111",
10898        )
10899        .expect("test operation should succeed");
10900        assert_eq!(
10901            parse_upload_pack_acknowledgment(ObjectFormat::Sha1, b"NAK\n")
10902                .expect("test operation should succeed"),
10903            UploadPackAcknowledgment::Nak
10904        );
10905        for (payload, status) in [
10906            (
10907                b"ACK 1111111111111111111111111111111111111111\n".as_slice(),
10908                None,
10909            ),
10910            (
10911                b"ACK 1111111111111111111111111111111111111111 continue\n".as_slice(),
10912                Some(UploadPackAckStatus::Continue),
10913            ),
10914            (
10915                b"ACK 1111111111111111111111111111111111111111 common\n".as_slice(),
10916                Some(UploadPackAckStatus::Common),
10917            ),
10918            (
10919                b"ACK 1111111111111111111111111111111111111111 ready\n".as_slice(),
10920                Some(UploadPackAckStatus::Ready),
10921            ),
10922        ] {
10923            let acknowledgment = parse_upload_pack_acknowledgment(ObjectFormat::Sha1, payload)
10924                .expect("test operation should succeed");
10925            assert_eq!(
10926                acknowledgment,
10927                UploadPackAcknowledgment::Ack { oid, status }
10928            );
10929            assert_eq!(
10930                encode_upload_pack_acknowledgment(&acknowledgment)
10931                    .expect("test operation should succeed"),
10932                payload
10933            );
10934        }
10935    }
10936
10937    #[test]
10938    fn upload_pack_acknowledgments_stream_round_trip_and_reject_bad_lines() {
10939        let acknowledgment = UploadPackAcknowledgment::Ack {
10940            oid: ObjectId::from_hex(
10941                ObjectFormat::Sha1,
10942                "1111111111111111111111111111111111111111",
10943            )
10944            .expect("test operation should succeed"),
10945            status: Some(UploadPackAckStatus::Ready),
10946        };
10947        let mut encoded = Vec::new();
10948        write_upload_pack_acknowledgment(&mut encoded, &acknowledgment)
10949            .expect("test operation should succeed");
10950        encoded.extend_from_slice(b"tail");
10951
10952        let mut input = encoded.as_slice();
10953        assert_eq!(
10954            read_upload_pack_acknowledgment(ObjectFormat::Sha1, &mut input)
10955                .expect("test operation should succeed"),
10956            acknowledgment
10957        );
10958        assert_eq!(input, b"tail");
10959        assert!(parse_upload_pack_acknowledgment(ObjectFormat::Sha1, b"ACK not-an-oid\n").is_err());
10960        assert!(
10961            parse_upload_pack_acknowledgment(
10962                ObjectFormat::Sha1,
10963                b"ACK 1111111111111111111111111111111111111111 unknown\n",
10964            )
10965            .is_err()
10966        );
10967        assert!(
10968            parse_upload_pack_acknowledgment(
10969                ObjectFormat::Sha1,
10970                b"ACK 1111111111111111111111111111111111111111 ready extra\n",
10971            )
10972            .is_err()
10973        );
10974        assert!(
10975            parse_upload_pack_acknowledgment(ObjectFormat::Sha1, b"ERR remote died\n").is_err()
10976        );
10977        assert!(read_upload_pack_acknowledgment(ObjectFormat::Sha1, &mut &b"0000"[..]).is_err());
10978    }
10979
10980    #[test]
10981    fn upload_pack_packfile_response_parses_acknowledgments_and_sideband() {
10982        let oid = ObjectId::from_hex(
10983            ObjectFormat::Sha1,
10984            "1111111111111111111111111111111111111111",
10985        )
10986        .expect("test operation should succeed");
10987        let frames = vec![
10988            PktLineFrame::Data(b"ACK 1111111111111111111111111111111111111111 common\n".to_vec()),
10989            PktLineFrame::Data(b"NAK\n".to_vec()),
10990            PktLineFrame::Data(b"\x01PACK".to_vec()),
10991            PktLineFrame::Data(b"\x02counting objects\n".to_vec()),
10992            PktLineFrame::Data(b"\x01 bytes".to_vec()),
10993            PktLineFrame::Flush,
10994        ];
10995        let response = parse_upload_pack_packfile_response(ObjectFormat::Sha1, &frames)
10996            .expect("test operation should succeed");
10997        assert_eq!(
10998            response,
10999            UploadPackPackfileResponse {
11000                acknowledgments: vec![
11001                    UploadPackAcknowledgment::Ack {
11002                        oid,
11003                        status: Some(UploadPackAckStatus::Common),
11004                    },
11005                    UploadPackAcknowledgment::Nak,
11006                ],
11007                sideband: vec![
11008                    SideBandPacket {
11009                        channel: SideBandChannel::Data,
11010                        data: b"PACK".to_vec(),
11011                    },
11012                    SideBandPacket {
11013                        channel: SideBandChannel::Progress,
11014                        data: b"counting objects\n".to_vec(),
11015                    },
11016                    SideBandPacket {
11017                        channel: SideBandChannel::Data,
11018                        data: b" bytes".to_vec(),
11019                    },
11020                ],
11021            }
11022        );
11023        assert_eq!(
11024            demux_upload_pack_packfile_response(&response).expect("test operation should succeed"),
11025            SideBandDemux {
11026                data: b"PACK bytes".to_vec(),
11027                progress: vec![b"counting objects\n".to_vec()],
11028            }
11029        );
11030        assert_eq!(
11031            encode_upload_pack_packfile_response(&response).expect("test operation should succeed"),
11032            frames
11033        );
11034    }
11035
11036    #[test]
11037    fn upload_pack_packfile_response_streams_round_trip() {
11038        let response = UploadPackPackfileResponse {
11039            acknowledgments: vec![UploadPackAcknowledgment::Nak],
11040            sideband: vec![SideBandPacket {
11041                channel: SideBandChannel::Data,
11042                data: b"PACK".to_vec(),
11043            }],
11044        };
11045        let mut encoded = Vec::new();
11046        write_upload_pack_packfile_response(&mut encoded, &response)
11047            .expect("test operation should succeed");
11048        encoded.extend_from_slice(b"tail");
11049
11050        let mut input = encoded.as_slice();
11051        assert_eq!(
11052            read_upload_pack_packfile_response(ObjectFormat::Sha1, &mut input)
11053                .expect("test operation should succeed"),
11054            response
11055        );
11056        assert_eq!(input, b"tail");
11057    }
11058
11059    #[test]
11060    fn upload_pack_packfile_response_rejects_malformed_streams() {
11061        assert!(
11062            parse_upload_pack_packfile_response(
11063                ObjectFormat::Sha1,
11064                &[PktLineFrame::Data(b"NAK\n".to_vec())],
11065            )
11066            .is_err()
11067        );
11068        assert!(
11069            parse_upload_pack_packfile_response(
11070                ObjectFormat::Sha1,
11071                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
11072            )
11073            .is_err()
11074        );
11075        assert!(
11076            parse_upload_pack_packfile_response(
11077                ObjectFormat::Sha1,
11078                &[
11079                    PktLineFrame::Data(b"\x01PACK".to_vec()),
11080                    PktLineFrame::Data(
11081                        b"ACK 1111111111111111111111111111111111111111 common\n".to_vec()
11082                    ),
11083                    PktLineFrame::Flush,
11084                ],
11085            )
11086            .is_err()
11087        );
11088        assert!(
11089            parse_upload_pack_packfile_response(
11090                ObjectFormat::Sha1,
11091                &[
11092                    PktLineFrame::Data(b"NAK\n".to_vec()),
11093                    PktLineFrame::Flush,
11094                    PktLineFrame::Data(b"\x01PACK".to_vec()),
11095                ],
11096            )
11097            .is_err()
11098        );
11099        assert!(
11100            parse_upload_pack_packfile_response(
11101                ObjectFormat::Sha1,
11102                &[
11103                    PktLineFrame::Data(b"NAK\n".to_vec()),
11104                    PktLineFrame::Data(b"\x04bad".to_vec()),
11105                    PktLineFrame::Flush,
11106                ],
11107            )
11108            .is_err()
11109        );
11110    }
11111
11112    #[test]
11113    fn upload_pack_raw_packfile_response_parses_acknowledgments_and_raw_pack() {
11114        let oid = ObjectId::from_hex(
11115            ObjectFormat::Sha1,
11116            "1111111111111111111111111111111111111111",
11117        )
11118        .expect("test operation should succeed");
11119        let response = UploadPackRawPackfileResponse {
11120            acknowledgments: vec![
11121                UploadPackAcknowledgment::Ack {
11122                    oid,
11123                    status: Some(UploadPackAckStatus::Common),
11124                },
11125                UploadPackAcknowledgment::Nak,
11126            ],
11127            packfile: b"PACK\x00\x00\x00\x02raw-bytes".to_vec(),
11128        };
11129        let encoded = encode_upload_pack_raw_packfile_response(&response)
11130            .expect("test operation should succeed");
11131        assert_eq!(
11132            parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &encoded)
11133                .expect("test operation should succeed"),
11134            response
11135        );
11136    }
11137
11138    #[test]
11139    fn upload_pack_raw_packfile_response_streams_round_trip() {
11140        let response = UploadPackRawPackfileResponse {
11141            acknowledgments: vec![UploadPackAcknowledgment::Nak],
11142            packfile: b"PACK\x00\x00\x00\x02raw-bytes".to_vec(),
11143        };
11144        let mut encoded = Vec::new();
11145        write_upload_pack_raw_packfile_response(&mut encoded, &response)
11146            .expect("test operation should succeed");
11147        assert_eq!(
11148            encoded,
11149            encode_upload_pack_raw_packfile_response(&response)
11150                .expect("test operation should succeed")
11151        );
11152
11153        let mut input = encoded.as_slice();
11154        assert_eq!(
11155            read_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &mut input)
11156                .expect("test operation should succeed"),
11157            response
11158        );
11159        assert!(input.is_empty());
11160    }
11161
11162    #[test]
11163    fn upload_pack_raw_packfile_response_rejects_malformed_streams() {
11164        let ack = PktLineFrame::data(b"NAK\n".to_vec())
11165            .expect("test operation should succeed")
11166            .try_encode()
11167            .expect("test operation should succeed");
11168        let bad_ack = PktLineFrame::data(b"ACK not-an-oid\n".to_vec())
11169            .expect("test operation should succeed")
11170            .try_encode()
11171            .expect("test operation should succeed");
11172        let non_ack =
11173            PktLineFrame::data(b"have 1111111111111111111111111111111111111111\n".to_vec())
11174                .expect("test operation should succeed")
11175                .try_encode()
11176                .expect("test operation should succeed");
11177        let mut garbage_after_ack = ack.clone();
11178        garbage_after_ack.extend_from_slice(b"garbage");
11179
11180        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, b"").is_err());
11181        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &ack).is_err());
11182        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &bad_ack).is_err());
11183        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, b"0000PACK").is_err());
11184        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &non_ack).is_err());
11185        assert!(
11186            parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &garbage_after_ack)
11187                .is_err()
11188        );
11189        assert!(
11190            encode_upload_pack_raw_packfile_response(&UploadPackRawPackfileResponse {
11191                acknowledgments: vec![UploadPackAcknowledgment::Nak],
11192                packfile: Vec::new(),
11193            })
11194            .is_err()
11195        );
11196        assert!(
11197            encode_upload_pack_raw_packfile_response(&UploadPackRawPackfileResponse {
11198                acknowledgments: Vec::new(),
11199                packfile: b"not-a-pack".to_vec(),
11200            })
11201            .is_err()
11202        );
11203    }
11204
11205    #[test]
11206    fn upload_pack_request_encodes_deepen_request() {
11207        // A `--depth 1` clone over smart-HTTP v1: the `want` line carries the
11208        // capabilities, the client's existing shallow boundary is replayed as a
11209        // `shallow` line, and `deepen 1` requests the truncation. Built as raw
11210        // pkt-line bytes so the 4-hex length prefixes are exercised.
11211        let want = ObjectId::from_hex(
11212            ObjectFormat::Sha1,
11213            "1111111111111111111111111111111111111111",
11214        )
11215        .expect("test operation should succeed");
11216        let boundary = ObjectId::from_hex(
11217            ObjectFormat::Sha1,
11218            "2222222222222222222222222222222222222222",
11219        )
11220        .expect("test operation should succeed");
11221        let request = UploadPackRequest {
11222            wants: vec![want],
11223            capabilities: vec![Capability {
11224                name: "shallow".into(),
11225                value: None,
11226            }],
11227            shallow: vec![boundary],
11228            deepen: Some(1),
11229            ..UploadPackRequest::default()
11230        };
11231        let mut encoded = Vec::new();
11232        write_upload_pack_request(&mut encoded, Some(&request))
11233            .expect("test operation should succeed");
11234        let mut expected = Vec::new();
11235        expected.extend_from_slice(b"003awant 1111111111111111111111111111111111111111 shallow\n");
11236        expected.extend_from_slice(b"0035shallow 2222222222222222222222222222222222222222\n");
11237        expected.extend_from_slice(b"000ddeepen 1\n");
11238        expected.extend_from_slice(b"0000");
11239        assert_eq!(encoded, expected);
11240    }
11241
11242    #[test]
11243    fn upload_pack_shallow_info_response_parses_shallow_unshallow_and_pack() {
11244        // The smart-HTTP v1 deepen response: a shallow-info section (one
11245        // `shallow` and one `unshallow` line) terminated by a flush, then the
11246        // NAK and the raw packfile. Hand-built pkt-lines (mind the lengths).
11247        let shallow = ObjectId::from_hex(
11248            ObjectFormat::Sha1,
11249            "1111111111111111111111111111111111111111",
11250        )
11251        .expect("test operation should succeed");
11252        let unshallow = ObjectId::from_hex(
11253            ObjectFormat::Sha1,
11254            "2222222222222222222222222222222222222222",
11255        )
11256        .expect("test operation should succeed");
11257        let mut input = Vec::new();
11258        input.extend_from_slice(b"0035shallow 1111111111111111111111111111111111111111\n");
11259        input.extend_from_slice(b"0037unshallow 2222222222222222222222222222222222222222\n");
11260        input.extend_from_slice(b"0000"); // shallow-info terminator
11261        input.extend_from_slice(b"0008NAK\n");
11262        input.extend_from_slice(b"PACK\x00\x00\x00\x02raw-bytes");
11263
11264        let (entries, response) =
11265            parse_upload_pack_shallow_info_and_raw_packfile_response(ObjectFormat::Sha1, &input)
11266                .expect("test operation should succeed");
11267        assert_eq!(
11268            entries,
11269            vec![
11270                ProtocolV2FetchShallowInfo::Shallow(shallow),
11271                ProtocolV2FetchShallowInfo::Unshallow(unshallow),
11272            ]
11273        );
11274        assert_eq!(
11275            response,
11276            UploadPackRawPackfileResponse {
11277                acknowledgments: vec![UploadPackAcknowledgment::Nak],
11278                packfile: b"PACK\x00\x00\x00\x02raw-bytes".to_vec(),
11279            }
11280        );
11281
11282        // The reader entry point yields the same result over a stream.
11283        let mut stream = input.as_slice();
11284        let (read_entries, read_response) =
11285            read_upload_pack_shallow_info_and_raw_packfile_response(
11286                ObjectFormat::Sha1,
11287                &mut stream,
11288            )
11289            .expect("test operation should succeed");
11290        assert_eq!(read_entries, entries);
11291        assert_eq!(read_response, response);
11292    }
11293
11294    #[test]
11295    fn upload_pack_shallow_info_response_handles_empty_shallow_section() {
11296        // A deepen request that creates no boundary change still gets an empty
11297        // shallow-info section (a bare flush) before the NAK + pack.
11298        let mut input = Vec::new();
11299        input.extend_from_slice(b"0000"); // empty shallow-info
11300        input.extend_from_slice(b"0008NAK\n");
11301        input.extend_from_slice(b"PACK\x00\x00\x00\x02raw-bytes");
11302
11303        let (entries, response) =
11304            parse_upload_pack_shallow_info_and_raw_packfile_response(ObjectFormat::Sha1, &input)
11305                .expect("test operation should succeed");
11306        assert!(entries.is_empty());
11307        assert_eq!(
11308            response.acknowledgments,
11309            vec![UploadPackAcknowledgment::Nak]
11310        );
11311        assert!(response.packfile.starts_with(b"PACK"));
11312    }
11313
11314    #[test]
11315    fn upload_pack_shallow_info_response_rejects_malformed_sections() {
11316        // Truncated section (no terminating flush before EOF).
11317        let truncated = b"0035shallow 1111111111111111111111111111111111111111\n".to_vec();
11318        assert!(
11319            parse_upload_pack_shallow_info_and_raw_packfile_response(
11320                ObjectFormat::Sha1,
11321                &truncated
11322            )
11323            .is_err()
11324        );
11325        // A non-flush control packet inside the shallow-info section.
11326        let mut delimiter_section = Vec::new();
11327        delimiter_section.extend_from_slice(b"0001"); // delimiter, not a flush
11328        assert!(
11329            parse_upload_pack_shallow_info_section(ObjectFormat::Sha1, &delimiter_section).is_err()
11330        );
11331        // A non-shallow data line inside the section.
11332        let mut bad_line = Vec::new();
11333        bad_line.extend_from_slice(b"0008NAK\n");
11334        assert!(parse_upload_pack_shallow_info_section(ObjectFormat::Sha1, &bad_line).is_err());
11335        // Valid shallow-info but a missing packfile afterwards.
11336        let mut no_pack = Vec::new();
11337        no_pack.extend_from_slice(b"0000"); // empty shallow-info
11338        no_pack.extend_from_slice(b"0008NAK\n");
11339        assert!(
11340            parse_upload_pack_shallow_info_and_raw_packfile_response(ObjectFormat::Sha1, &no_pack)
11341                .is_err()
11342        );
11343    }
11344
11345    #[test]
11346    fn receive_pack_request_parses_and_encodes_commands() {
11347        let old_id = ObjectId::from_hex(
11348            ObjectFormat::Sha1,
11349            "1111111111111111111111111111111111111111",
11350        )
11351        .expect("test operation should succeed");
11352        let new_id = ObjectId::from_hex(
11353            ObjectFormat::Sha1,
11354            "2222222222222222222222222222222222222222",
11355        )
11356        .expect("test operation should succeed");
11357        let delete_old_id = ObjectId::from_hex(
11358            ObjectFormat::Sha1,
11359            "3333333333333333333333333333333333333333",
11360        )
11361        .expect("test operation should succeed");
11362        let zero = ObjectId::from_hex(
11363            ObjectFormat::Sha1,
11364            "0000000000000000000000000000000000000000",
11365        )
11366        .expect("test operation should succeed");
11367        let shallow = ObjectId::from_hex(
11368            ObjectFormat::Sha1,
11369            "4444444444444444444444444444444444444444",
11370        )
11371        .expect("test operation should succeed");
11372        let frames = vec![
11373            PktLineFrame::Data(b"shallow 4444444444444444444444444444444444444444\n".to_vec()),
11374            PktLineFrame::Data(
11375                b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\0report-status side-band-64k agent=git/2.54.0\n"
11376                    .to_vec(),
11377            ),
11378            PktLineFrame::Data(
11379                b"3333333333333333333333333333333333333333 0000000000000000000000000000000000000000 refs/heads/old\n"
11380                    .to_vec(),
11381            ),
11382            PktLineFrame::Flush,
11383        ];
11384        let request = parse_receive_pack_request(ObjectFormat::Sha1, &frames)
11385            .expect("test operation should succeed");
11386        assert_eq!(
11387            request,
11388            ReceivePackRequest {
11389                shallow: vec![shallow],
11390                commands: vec![
11391                    ReceivePackCommand {
11392                        old_id,
11393                        new_id,
11394                        name: "refs/heads/main".into(),
11395                    },
11396                    ReceivePackCommand {
11397                        old_id: delete_old_id,
11398                        new_id: zero,
11399                        name: "refs/heads/old".into(),
11400                    },
11401                ],
11402                capabilities: vec![
11403                    Capability {
11404                        name: "report-status".into(),
11405                        value: None,
11406                    },
11407                    Capability {
11408                        name: "side-band-64k".into(),
11409                        value: None,
11410                    },
11411                    Capability {
11412                        name: "agent".into(),
11413                        value: Some("git/2.54.0".into()),
11414                    },
11415                ],
11416            }
11417        );
11418        assert_eq!(
11419            encode_receive_pack_request(&request).expect("test operation should succeed"),
11420            frames
11421        );
11422        assert_eq!(
11423            parse_receive_pack_request(ObjectFormat::Sha1, &[PktLineFrame::Flush])
11424                .expect("test operation should succeed"),
11425            ReceivePackRequest::default()
11426        );
11427    }
11428
11429    #[test]
11430    fn receive_pack_request_streams_round_trip() {
11431        let request = ReceivePackRequest {
11432            commands: vec![ReceivePackCommand {
11433                old_id: ObjectId::from_hex(
11434                    ObjectFormat::Sha1,
11435                    "0000000000000000000000000000000000000000",
11436                )
11437                .expect("test operation should succeed"),
11438                new_id: ObjectId::from_hex(
11439                    ObjectFormat::Sha1,
11440                    "1111111111111111111111111111111111111111",
11441                )
11442                .expect("test operation should succeed"),
11443                name: "refs/heads/main".into(),
11444            }],
11445            capabilities: vec![Capability {
11446                name: "report-status".into(),
11447                value: None,
11448            }],
11449            ..ReceivePackRequest::default()
11450        };
11451        let mut encoded = Vec::new();
11452        write_receive_pack_request(&mut encoded, &request).expect("test operation should succeed");
11453        encoded.extend_from_slice(b"PACK");
11454
11455        let mut input = encoded.as_slice();
11456        assert_eq!(
11457            read_receive_pack_request(ObjectFormat::Sha1, &mut input)
11458                .expect("test operation should succeed"),
11459            request
11460        );
11461        assert_eq!(input, b"PACK");
11462    }
11463
11464    #[test]
11465    fn receive_pack_request_rejects_malformed_commands() {
11466        assert!(
11467            parse_receive_pack_request(
11468                ObjectFormat::Sha1,
11469                &[PktLineFrame::Data(
11470                    b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\n"
11471                        .to_vec(),
11472                )],
11473            )
11474            .is_err()
11475        );
11476        assert!(
11477            parse_receive_pack_request(
11478                ObjectFormat::Sha1,
11479                &[
11480                    PktLineFrame::Data(
11481                        b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\n"
11482                            .to_vec(),
11483                    ),
11484                    PktLineFrame::Data(
11485                        b"shallow 3333333333333333333333333333333333333333\n".to_vec(),
11486                    ),
11487                    PktLineFrame::Flush,
11488                ],
11489            )
11490            .is_err()
11491        );
11492        assert!(
11493            parse_receive_pack_request(
11494                ObjectFormat::Sha1,
11495                &[
11496                    PktLineFrame::Data(
11497                        b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\0report-status\n"
11498                            .to_vec(),
11499                    ),
11500                    PktLineFrame::Data(
11501                        b"3333333333333333333333333333333333333333 4444444444444444444444444444444444444444 refs/heads/next\0side-band-64k\n"
11502                            .to_vec(),
11503                    ),
11504                    PktLineFrame::Flush,
11505                ],
11506            )
11507            .is_err()
11508        );
11509        assert!(
11510            parse_receive_pack_request(
11511                ObjectFormat::Sha1,
11512                &[
11513                    PktLineFrame::Data(
11514                        b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec(),
11515                    ),
11516                    PktLineFrame::Flush,
11517                ],
11518            )
11519            .is_err()
11520        );
11521        assert!(
11522            encode_receive_pack_request(&ReceivePackRequest {
11523                shallow: vec![
11524                    ObjectId::from_hex(
11525                        ObjectFormat::Sha1,
11526                        "1111111111111111111111111111111111111111",
11527                    )
11528                    .expect("test operation should succeed")
11529                ],
11530                ..ReceivePackRequest::default()
11531            })
11532            .is_err()
11533        );
11534        assert!(
11535            encode_receive_pack_request(&ReceivePackRequest {
11536                commands: vec![ReceivePackCommand {
11537                    old_id: ObjectId::from_hex(
11538                        ObjectFormat::Sha1,
11539                        "1111111111111111111111111111111111111111",
11540                    )
11541                    .expect("test operation should succeed"),
11542                    new_id: ObjectId::from_hex(
11543                        ObjectFormat::Sha1,
11544                        "2222222222222222222222222222222222222222",
11545                    )
11546                    .expect("test operation should succeed"),
11547                    name: "bad ref".into(),
11548                }],
11549                ..ReceivePackRequest::default()
11550            })
11551            .is_err()
11552        );
11553    }
11554
11555    #[test]
11556    fn receive_pack_features_parse_encode_and_validate_push_request() {
11557        let capabilities = vec![
11558            Capability {
11559                name: "report-status".into(),
11560                value: None,
11561            },
11562            Capability {
11563                name: "report-status-v2".into(),
11564                value: None,
11565            },
11566            Capability {
11567                name: "delete-refs".into(),
11568                value: None,
11569            },
11570            Capability {
11571                name: "ofs-delta".into(),
11572                value: None,
11573            },
11574            Capability {
11575                name: "atomic".into(),
11576                value: None,
11577            },
11578            Capability {
11579                name: "push-options".into(),
11580                value: None,
11581            },
11582            Capability {
11583                name: "side-band-64k".into(),
11584                value: None,
11585            },
11586            Capability {
11587                name: "quiet".into(),
11588                value: None,
11589            },
11590            Capability {
11591                name: "no-thin".into(),
11592                value: None,
11593            },
11594            Capability {
11595                name: "agent".into(),
11596                value: Some("git/2.54.0".into()),
11597            },
11598            Capability {
11599                name: "object-format".into(),
11600                value: Some("sha256".into()),
11601            },
11602            Capability {
11603                name: "custom".into(),
11604                value: Some("value".into()),
11605            },
11606        ];
11607        let features =
11608            parse_receive_pack_features(&capabilities).expect("test operation should succeed");
11609        assert_eq!(
11610            features,
11611            ReceivePackFeatures {
11612                report_status: true,
11613                report_status_v2: true,
11614                delete_refs: true,
11615                ofs_delta: true,
11616                atomic: true,
11617                push_options: true,
11618                side_band_64k: true,
11619                quiet: true,
11620                no_thin: true,
11621                agent: Some("git/2.54.0".into()),
11622                object_format: Some(ObjectFormat::Sha256),
11623                unknown: vec![Capability {
11624                    name: "custom".into(),
11625                    value: Some("value".into()),
11626                }],
11627            }
11628        );
11629        assert_eq!(
11630            encode_receive_pack_features(&features).expect("test operation should succeed"),
11631            capabilities
11632        );
11633
11634        let request = ReceivePackPushRequest {
11635            commands: ReceivePackRequest {
11636                commands: vec![ReceivePackCommand {
11637                    old_id: ObjectId::from_hex(
11638                        ObjectFormat::Sha1,
11639                        "1111111111111111111111111111111111111111",
11640                    )
11641                    .expect("test operation should succeed"),
11642                    new_id: ObjectId::from_hex(
11643                        ObjectFormat::Sha1,
11644                        "2222222222222222222222222222222222222222",
11645                    )
11646                    .expect("test operation should succeed"),
11647                    name: "refs/heads/main".into(),
11648                }],
11649                capabilities: vec![
11650                    Capability {
11651                        name: "report-status".into(),
11652                        value: None,
11653                    },
11654                    Capability {
11655                        name: "ofs-delta".into(),
11656                        value: None,
11657                    },
11658                    Capability {
11659                        name: "push-options".into(),
11660                        value: None,
11661                    },
11662                    Capability {
11663                        name: "side-band-64k".into(),
11664                        value: None,
11665                    },
11666                    Capability {
11667                        name: "agent".into(),
11668                        value: Some("sley".into()),
11669                    },
11670                ],
11671                ..ReceivePackRequest::default()
11672            },
11673            push_options: Some(vec!["ci.skip".into()]),
11674            packfile: b"PACKpayload".to_vec(),
11675        };
11676        validate_receive_pack_push_request_features(&features, &request)
11677            .expect("test operation should succeed");
11678    }
11679
11680    #[test]
11681    fn receive_pack_features_reject_invalid_push_requests() {
11682        let old_id = ObjectId::from_hex(
11683            ObjectFormat::Sha1,
11684            "1111111111111111111111111111111111111111",
11685        )
11686        .expect("test operation should succeed");
11687        let new_id = ObjectId::from_hex(
11688            ObjectFormat::Sha1,
11689            "2222222222222222222222222222222222222222",
11690        )
11691        .expect("test operation should succeed");
11692        let zero = ObjectId::from_hex(
11693            ObjectFormat::Sha1,
11694            "0000000000000000000000000000000000000000",
11695        )
11696        .expect("test operation should succeed");
11697        let features = ReceivePackFeatures {
11698            report_status: true,
11699            push_options: true,
11700            ..ReceivePackFeatures::default()
11701        };
11702        let update = ReceivePackCommand {
11703            old_id: old_id.clone(),
11704            new_id: new_id.clone(),
11705            name: "refs/heads/main".into(),
11706        };
11707
11708        assert!(
11709            validate_receive_pack_push_request_features(
11710                &features,
11711                &ReceivePackPushRequest {
11712                    commands: ReceivePackRequest {
11713                        commands: vec![update.clone()],
11714                        capabilities: vec![Capability {
11715                            name: "push-options".into(),
11716                            value: None,
11717                        }],
11718                        ..ReceivePackRequest::default()
11719                    },
11720                    push_options: None,
11721                    packfile: b"PACKpayload".to_vec(),
11722                },
11723            )
11724            .is_err()
11725        );
11726        assert!(
11727            validate_receive_pack_push_request_features(
11728                &features,
11729                &ReceivePackPushRequest {
11730                    commands: ReceivePackRequest {
11731                        commands: vec![update.clone()],
11732                        ..ReceivePackRequest::default()
11733                    },
11734                    push_options: Some(Vec::new()),
11735                    packfile: b"PACKpayload".to_vec(),
11736                },
11737            )
11738            .is_err()
11739        );
11740        assert!(
11741            validate_receive_pack_push_request_features(
11742                &features,
11743                &ReceivePackPushRequest {
11744                    commands: ReceivePackRequest {
11745                        commands: vec![ReceivePackCommand {
11746                            old_id: old_id.clone(),
11747                            new_id: zero.clone(),
11748                            name: "refs/heads/main".into(),
11749                        }],
11750                        ..ReceivePackRequest::default()
11751                    },
11752                    push_options: None,
11753                    packfile: Vec::new(),
11754                },
11755            )
11756            .is_err()
11757        );
11758        assert!(
11759            validate_receive_pack_push_request_features(
11760                &features,
11761                &ReceivePackPushRequest {
11762                    commands: ReceivePackRequest {
11763                        commands: vec![update.clone()],
11764                        ..ReceivePackRequest::default()
11765                    },
11766                    push_options: None,
11767                    packfile: Vec::new(),
11768                },
11769            )
11770            .is_err()
11771        );
11772        assert!(
11773            validate_receive_pack_push_request_features(
11774                &ReceivePackFeatures {
11775                    delete_refs: true,
11776                    ..ReceivePackFeatures::default()
11777                },
11778                &ReceivePackPushRequest {
11779                    commands: ReceivePackRequest {
11780                        commands: vec![ReceivePackCommand {
11781                            old_id,
11782                            new_id: zero,
11783                            name: "refs/heads/main".into(),
11784                        }],
11785                        ..ReceivePackRequest::default()
11786                    },
11787                    push_options: None,
11788                    packfile: b"PACKpayload".to_vec(),
11789                },
11790            )
11791            .is_err()
11792        );
11793        assert!(
11794            validate_receive_pack_push_request_features(
11795                &features,
11796                &ReceivePackPushRequest {
11797                    commands: ReceivePackRequest {
11798                        commands: vec![update],
11799                        capabilities: vec![Capability {
11800                            name: "atomic".into(),
11801                            value: None,
11802                        }],
11803                        ..ReceivePackRequest::default()
11804                    },
11805                    push_options: None,
11806                    packfile: b"PACKpayload".to_vec(),
11807                },
11808            )
11809            .is_err()
11810        );
11811
11812        assert!(
11813            parse_receive_pack_features(&[
11814                Capability {
11815                    name: "push-options".into(),
11816                    value: None,
11817                },
11818                Capability {
11819                    name: "push-options".into(),
11820                    value: None,
11821                },
11822            ])
11823            .is_err()
11824        );
11825        assert!(
11826            encode_receive_pack_features(&ReceivePackFeatures {
11827                unknown: vec![Capability {
11828                    name: "atomic".into(),
11829                    value: None,
11830                }],
11831                ..ReceivePackFeatures::default()
11832            })
11833            .is_err()
11834        );
11835    }
11836
11837    #[test]
11838    fn receive_pack_apply_helper_installs_pack_verifies_objects_and_reports_ok() {
11839        let old_id = ObjectId::from_hex(
11840            ObjectFormat::Sha1,
11841            "1111111111111111111111111111111111111111",
11842        )
11843        .expect("test operation should succeed");
11844        let new_id = ObjectId::from_hex(
11845            ObjectFormat::Sha1,
11846            "2222222222222222222222222222222222222222",
11847        )
11848        .expect("test operation should succeed");
11849        let request = ReceivePackPushRequest {
11850            commands: ReceivePackRequest {
11851                commands: vec![ReceivePackCommand {
11852                    old_id: old_id.clone(),
11853                    new_id: new_id.clone(),
11854                    name: "refs/heads/main".into(),
11855                }],
11856                ..ReceivePackRequest::default()
11857            },
11858            packfile: b"PACKpayload".to_vec(),
11859            ..ReceivePackPushRequest::default()
11860        };
11861        let installed = std::cell::Cell::new(false);
11862        let applied = std::cell::RefCell::new(Vec::new());
11863
11864        let report = apply_receive_pack_push_request(
11865            &ReceivePackFeatures::default(),
11866            &request,
11867            |_| unreachable!("update stale-old checks belong to the ref transaction callback"),
11868            |packfile| {
11869                assert_eq!(packfile, b"PACKpayload");
11870                installed.set(true);
11871                Ok(())
11872            },
11873            |oid| Ok(oid == &new_id),
11874            |commands| {
11875                applied.borrow_mut().extend_from_slice(commands);
11876                Ok(())
11877            },
11878            |_| unreachable!("no delete command should be applied"),
11879        )
11880        .expect("test operation should succeed");
11881
11882        assert!(installed.get());
11883        assert_eq!(applied.into_inner(), request.commands.commands);
11884        assert_eq!(report.unpack, ReceivePackUnpackStatus::Ok);
11885        assert_eq!(
11886            report.commands,
11887            vec![ReceivePackCommandStatus::Ok {
11888                name: "refs/heads/main".into(),
11889            }]
11890        );
11891    }
11892
11893    #[test]
11894    fn receive_pack_apply_helper_preserves_delete_only_and_stale_delete_rules() {
11895        let old_id = ObjectId::from_hex(
11896            ObjectFormat::Sha1,
11897            "1111111111111111111111111111111111111111",
11898        )
11899        .expect("test operation should succeed");
11900        let other_id = ObjectId::from_hex(
11901            ObjectFormat::Sha1,
11902            "2222222222222222222222222222222222222222",
11903        )
11904        .expect("test operation should succeed");
11905        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
11906        let request = ReceivePackPushRequest {
11907            commands: ReceivePackRequest {
11908                commands: vec![ReceivePackCommand {
11909                    old_id: old_id.clone(),
11910                    new_id: zero,
11911                    name: "refs/heads/main".into(),
11912                }],
11913                ..ReceivePackRequest::default()
11914            },
11915            ..ReceivePackPushRequest::default()
11916        };
11917        let features = ReceivePackFeatures {
11918            delete_refs: true,
11919            ..ReceivePackFeatures::default()
11920        };
11921        let installed = std::cell::Cell::new(false);
11922        let deleted = std::cell::RefCell::new(Vec::new());
11923
11924        let report = apply_receive_pack_push_request(
11925            &features,
11926            &request,
11927            |_| Ok(Some(old_id.clone())),
11928            |_| {
11929                installed.set(true);
11930                Ok(())
11931            },
11932            |_| Ok(false),
11933            |_| unreachable!("delete-only request should not apply updates"),
11934            |command| {
11935                deleted.borrow_mut().push(command.name.clone());
11936                Ok(())
11937            },
11938        )
11939        .expect("test operation should succeed");
11940
11941        assert!(!installed.get());
11942        assert_eq!(deleted.into_inner(), vec!["refs/heads/main"]);
11943        assert_eq!(report.unpack, ReceivePackUnpackStatus::Ok);
11944        assert!(
11945            apply_receive_pack_push_request(
11946                &features,
11947                &request,
11948                |_| Ok(Some(other_id.clone())),
11949                |_| Ok(()),
11950                |_| Ok(false),
11951                |_| Ok(()),
11952                |_| Ok(()),
11953            )
11954            .is_err()
11955        );
11956    }
11957
11958    #[test]
11959    fn receive_pack_push_request_parses_commands_options_and_packfile() {
11960        let command = ReceivePackCommand {
11961            old_id: ObjectId::from_hex(
11962                ObjectFormat::Sha1,
11963                "1111111111111111111111111111111111111111",
11964            )
11965            .expect("test operation should succeed"),
11966            new_id: ObjectId::from_hex(
11967                ObjectFormat::Sha1,
11968                "2222222222222222222222222222222222222222",
11969            )
11970            .expect("test operation should succeed"),
11971            name: "refs/heads/main".into(),
11972        };
11973        let expected = ReceivePackPushRequest {
11974            commands: ReceivePackRequest {
11975                commands: vec![command],
11976                capabilities: vec![
11977                    Capability {
11978                        name: "report-status".into(),
11979                        value: None,
11980                    },
11981                    Capability {
11982                        name: "push-options".into(),
11983                        value: None,
11984                    },
11985                ],
11986                ..ReceivePackRequest::default()
11987            },
11988            push_options: Some(vec!["ci.skip".into(), "deploy=staging".into()]),
11989            packfile: b"PACK\x00\x00\x00\x02payload".to_vec(),
11990        };
11991        let encoded =
11992            encode_receive_pack_push_request(&expected).expect("test operation should succeed");
11993
11994        assert_eq!(
11995            parse_receive_pack_push_request(ObjectFormat::Sha1, &encoded, true)
11996                .expect("test operation should succeed"),
11997            expected
11998        );
11999    }
12000
12001    #[test]
12002    fn receive_pack_push_request_preserves_packfile_without_push_options() {
12003        let request = ReceivePackPushRequest {
12004            commands: ReceivePackRequest {
12005                commands: vec![ReceivePackCommand {
12006                    old_id: ObjectId::from_hex(
12007                        ObjectFormat::Sha1,
12008                        "1111111111111111111111111111111111111111",
12009                    )
12010                    .expect("test operation should succeed"),
12011                    new_id: ObjectId::from_hex(
12012                        ObjectFormat::Sha1,
12013                        "2222222222222222222222222222222222222222",
12014                    )
12015                    .expect("test operation should succeed"),
12016                    name: "refs/heads/main".into(),
12017                }],
12018                ..ReceivePackRequest::default()
12019            },
12020            push_options: None,
12021            packfile: b"0000PACK-like bytes stay raw".to_vec(),
12022        };
12023        let encoded =
12024            encode_receive_pack_push_request(&request).expect("test operation should succeed");
12025
12026        assert_eq!(
12027            parse_receive_pack_push_request(ObjectFormat::Sha1, &encoded, false)
12028                .expect("test operation should succeed"),
12029            request
12030        );
12031    }
12032
12033    #[test]
12034    fn receive_pack_push_request_streams_round_trip() {
12035        let request = ReceivePackPushRequest {
12036            commands: ReceivePackRequest {
12037                commands: vec![ReceivePackCommand {
12038                    old_id: ObjectId::from_hex(
12039                        ObjectFormat::Sha1,
12040                        "1111111111111111111111111111111111111111",
12041                    )
12042                    .expect("test operation should succeed"),
12043                    new_id: ObjectId::from_hex(
12044                        ObjectFormat::Sha1,
12045                        "2222222222222222222222222222222222222222",
12046                    )
12047                    .expect("test operation should succeed"),
12048                    name: "refs/heads/main".into(),
12049                }],
12050                capabilities: vec![Capability {
12051                    name: "push-options".into(),
12052                    value: None,
12053                }],
12054                ..ReceivePackRequest::default()
12055            },
12056            push_options: Some(Vec::new()),
12057            packfile: b"PACKpayload".to_vec(),
12058        };
12059        let mut encoded = Vec::new();
12060        write_receive_pack_push_request(&mut encoded, &request)
12061            .expect("test operation should succeed");
12062
12063        assert_eq!(
12064            read_receive_pack_push_request(ObjectFormat::Sha1, &mut encoded.as_slice(), true)
12065                .expect("test operation should succeed"),
12066            request
12067        );
12068    }
12069
12070    #[test]
12071    fn receive_pack_push_request_rejects_malformed_sections() {
12072        assert!(
12073            parse_receive_pack_push_request(
12074                ObjectFormat::Sha1,
12075                b"0014not-a-command\n0000PACK",
12076                false,
12077            )
12078            .is_err()
12079        );
12080
12081        let request = ReceivePackPushRequest {
12082            commands: ReceivePackRequest {
12083                commands: vec![ReceivePackCommand {
12084                    old_id: ObjectId::from_hex(
12085                        ObjectFormat::Sha1,
12086                        "1111111111111111111111111111111111111111",
12087                    )
12088                    .expect("test operation should succeed"),
12089                    new_id: ObjectId::from_hex(
12090                        ObjectFormat::Sha1,
12091                        "2222222222222222222222222222222222222222",
12092                    )
12093                    .expect("test operation should succeed"),
12094                    name: "refs/heads/main".into(),
12095                }],
12096                ..ReceivePackRequest::default()
12097            },
12098            push_options: None,
12099            packfile: b"PACKpayload".to_vec(),
12100        };
12101        let encoded =
12102            encode_receive_pack_push_request(&request).expect("test operation should succeed");
12103        assert!(parse_receive_pack_push_request(ObjectFormat::Sha1, &encoded, true).is_err());
12104
12105        assert!(
12106            encode_receive_pack_push_request(&ReceivePackPushRequest {
12107                commands: ReceivePackRequest {
12108                    shallow: vec![
12109                        ObjectId::from_hex(
12110                            ObjectFormat::Sha1,
12111                            "1111111111111111111111111111111111111111",
12112                        )
12113                        .expect("test operation should succeed")
12114                    ],
12115                    ..ReceivePackRequest::default()
12116                },
12117                push_options: None,
12118                packfile: Vec::new(),
12119            })
12120            .is_err()
12121        );
12122    }
12123
12124    #[test]
12125    fn receive_pack_report_status_parses_and_encodes_status_lines() {
12126        let frames = vec![
12127            PktLineFrame::Data(b"unpack ok\n".to_vec()),
12128            PktLineFrame::Data(b"ok refs/heads/main\n".to_vec()),
12129            PktLineFrame::Data(b"ng refs/heads/old non-fast-forward\n".to_vec()),
12130            PktLineFrame::Flush,
12131        ];
12132        let report =
12133            parse_receive_pack_report_status(&frames).expect("test operation should succeed");
12134        assert_eq!(
12135            report,
12136            ReceivePackReportStatus {
12137                unpack: ReceivePackUnpackStatus::Ok,
12138                commands: vec![
12139                    ReceivePackCommandStatus::Ok {
12140                        name: "refs/heads/main".into(),
12141                    },
12142                    ReceivePackCommandStatus::Ng {
12143                        name: "refs/heads/old".into(),
12144                        message: "non-fast-forward".into(),
12145                    },
12146                ],
12147            }
12148        );
12149        assert_eq!(
12150            encode_receive_pack_report_status(&report).expect("test operation should succeed"),
12151            frames
12152        );
12153
12154        let frames = vec![
12155            PktLineFrame::Data(b"unpack pack exceeds maximum size\n".to_vec()),
12156            PktLineFrame::Flush,
12157        ];
12158        assert_eq!(
12159            parse_receive_pack_report_status(&frames).expect("test operation should succeed"),
12160            ReceivePackReportStatus {
12161                unpack: ReceivePackUnpackStatus::Error("pack exceeds maximum size".into()),
12162                commands: Vec::new(),
12163            }
12164        );
12165    }
12166
12167    #[test]
12168    fn receive_pack_report_status_streams_round_trip() {
12169        let report = ReceivePackReportStatus {
12170            unpack: ReceivePackUnpackStatus::Ok,
12171            commands: vec![ReceivePackCommandStatus::Ok {
12172                name: "refs/heads/main".into(),
12173            }],
12174        };
12175        let mut encoded = Vec::new();
12176        write_receive_pack_report_status(&mut encoded, &report)
12177            .expect("test operation should succeed");
12178        encoded.extend_from_slice(b"tail");
12179
12180        let mut input = encoded.as_slice();
12181        assert_eq!(
12182            read_receive_pack_report_status(&mut input).expect("test operation should succeed"),
12183            report
12184        );
12185        assert_eq!(input, b"tail");
12186    }
12187
12188    #[test]
12189    fn receive_pack_report_status_rejects_malformed_status_lines() {
12190        assert!(parse_receive_pack_report_status(&[]).is_err());
12191        assert!(
12192            parse_receive_pack_report_status(&[
12193                PktLineFrame::Data(b"unpack ok\n".to_vec()),
12194                PktLineFrame::Data(b"ok refs/heads/main\n".to_vec()),
12195            ])
12196            .is_err()
12197        );
12198        assert!(
12199            parse_receive_pack_report_status(&[
12200                PktLineFrame::Flush,
12201                PktLineFrame::Data(b"ok refs/heads/main\n".to_vec()),
12202            ])
12203            .is_err()
12204        );
12205        assert!(
12206            parse_receive_pack_report_status(&[
12207                PktLineFrame::Data(b"unpack ok\n".to_vec()),
12208                PktLineFrame::Data(b"bad refs/heads/main\n".to_vec()),
12209                PktLineFrame::Flush,
12210            ])
12211            .is_err()
12212        );
12213        assert!(
12214            parse_receive_pack_report_status(&[
12215                PktLineFrame::Data(b"unpack ok\n".to_vec()),
12216                PktLineFrame::Data(b"ng refs/heads/main\n".to_vec()),
12217                PktLineFrame::Flush,
12218            ])
12219            .is_err()
12220        );
12221        assert!(
12222            encode_receive_pack_report_status(&ReceivePackReportStatus {
12223                unpack: ReceivePackUnpackStatus::Error("".into()),
12224                commands: Vec::new(),
12225            })
12226            .is_err()
12227        );
12228        assert!(
12229            encode_receive_pack_report_status(&ReceivePackReportStatus {
12230                unpack: ReceivePackUnpackStatus::Ok,
12231                commands: vec![ReceivePackCommandStatus::Ok {
12232                    name: "bad ref".into(),
12233                }],
12234            })
12235            .is_err()
12236        );
12237    }
12238
12239    #[test]
12240    fn receive_pack_report_status_v2_parses_and_encodes_options() {
12241        let old_oid = ObjectId::from_hex(
12242            ObjectFormat::Sha1,
12243            "1111111111111111111111111111111111111111",
12244        )
12245        .expect("test operation should succeed");
12246        let new_oid = ObjectId::from_hex(
12247            ObjectFormat::Sha1,
12248            "2222222222222222222222222222222222222222",
12249        )
12250        .expect("test operation should succeed");
12251        let frames = vec![
12252            PktLineFrame::Data(b"unpack ok\n".to_vec()),
12253            PktLineFrame::Data(b"ok refs/for/main\n".to_vec()),
12254            PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12255            PktLineFrame::Data(
12256                b"option old-oid 1111111111111111111111111111111111111111\n".to_vec(),
12257            ),
12258            PktLineFrame::Data(
12259                b"option new-oid 2222222222222222222222222222222222222222\n".to_vec(),
12260            ),
12261            PktLineFrame::Data(b"option forced-update\n".to_vec()),
12262            PktLineFrame::Data(b"ng refs/heads/old rejected by hook\n".to_vec()),
12263            PktLineFrame::Flush,
12264        ];
12265        let report = parse_receive_pack_report_status_v2(ObjectFormat::Sha1, &frames)
12266            .expect("test operation should succeed");
12267        assert_eq!(
12268            report,
12269            ReceivePackReportStatusV2 {
12270                unpack: ReceivePackUnpackStatus::Ok,
12271                commands: vec![
12272                    ReceivePackCommandStatusV2::Ok {
12273                        name: "refs/for/main".into(),
12274                        options: ReceivePackCommandStatusV2Options {
12275                            refname: Some("refs/heads/main".into()),
12276                            old_oid: Some(old_oid),
12277                            new_oid: Some(new_oid),
12278                            forced_update: true,
12279                        },
12280                    },
12281                    ReceivePackCommandStatusV2::Ng {
12282                        name: "refs/heads/old".into(),
12283                        message: "rejected by hook".into(),
12284                    },
12285                ],
12286            }
12287        );
12288        assert_eq!(
12289            encode_receive_pack_report_status_v2(&report).expect("test operation should succeed"),
12290            frames
12291        );
12292    }
12293
12294    #[test]
12295    fn receive_pack_report_status_v2_streams_round_trip() {
12296        let report = ReceivePackReportStatusV2 {
12297            unpack: ReceivePackUnpackStatus::Ok,
12298            commands: vec![ReceivePackCommandStatusV2::Ok {
12299                name: "refs/for/main".into(),
12300                options: ReceivePackCommandStatusV2Options {
12301                    refname: Some("refs/heads/main".into()),
12302                    old_oid: None,
12303                    new_oid: None,
12304                    forced_update: false,
12305                },
12306            }],
12307        };
12308        let mut encoded = Vec::new();
12309        write_receive_pack_report_status_v2(&mut encoded, &report)
12310            .expect("test operation should succeed");
12311        encoded.extend_from_slice(b"tail");
12312
12313        let mut input = encoded.as_slice();
12314        assert_eq!(
12315            read_receive_pack_report_status_v2(ObjectFormat::Sha1, &mut input)
12316                .expect("test operation should succeed"),
12317            report
12318        );
12319        assert_eq!(input, b"tail");
12320    }
12321
12322    #[test]
12323    fn receive_pack_report_status_v2_rejects_malformed_options() {
12324        assert!(parse_receive_pack_report_status_v2(ObjectFormat::Sha1, &[]).is_err());
12325        assert!(
12326            parse_receive_pack_report_status_v2(
12327                ObjectFormat::Sha1,
12328                &[
12329                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12330                    PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12331                    PktLineFrame::Flush,
12332                ],
12333            )
12334            .is_err()
12335        );
12336        assert!(
12337            parse_receive_pack_report_status_v2(
12338                ObjectFormat::Sha1,
12339                &[
12340                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12341                    PktLineFrame::Data(b"ng refs/heads/main rejected\n".to_vec()),
12342                    PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12343                    PktLineFrame::Flush,
12344                ],
12345            )
12346            .is_err()
12347        );
12348        assert!(
12349            parse_receive_pack_report_status_v2(
12350                ObjectFormat::Sha1,
12351                &[
12352                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12353                    PktLineFrame::Data(b"ok refs/for/main\n".to_vec()),
12354                    PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12355                    PktLineFrame::Data(b"option refname refs/heads/next\n".to_vec()),
12356                    PktLineFrame::Flush,
12357                ],
12358            )
12359            .is_err()
12360        );
12361        assert!(
12362            parse_receive_pack_report_status_v2(
12363                ObjectFormat::Sha1,
12364                &[
12365                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12366                    PktLineFrame::Data(b"ok refs/for/main\n".to_vec()),
12367                    PktLineFrame::Data(b"option old-oid not-an-oid\n".to_vec()),
12368                    PktLineFrame::Flush,
12369                ],
12370            )
12371            .is_err()
12372        );
12373        assert!(
12374            encode_receive_pack_report_status_v2(&ReceivePackReportStatusV2 {
12375                unpack: ReceivePackUnpackStatus::Ok,
12376                commands: vec![ReceivePackCommandStatusV2::Ok {
12377                    name: "refs/for/main".into(),
12378                    options: ReceivePackCommandStatusV2Options {
12379                        refname: Some("bad ref".into()),
12380                        ..ReceivePackCommandStatusV2Options::default()
12381                    },
12382                }],
12383            })
12384            .is_err()
12385        );
12386    }
12387
12388    #[test]
12389    fn receive_pack_push_options_parse_and_encode_options() {
12390        let frames = vec![
12391            PktLineFrame::Data(b"ci.skip\n".to_vec()),
12392            PktLineFrame::Data(b"deploy target=staging\n".to_vec()),
12393            PktLineFrame::Data(b"\n".to_vec()),
12394            PktLineFrame::Flush,
12395        ];
12396        let options =
12397            parse_receive_pack_push_options(&frames).expect("test operation should succeed");
12398        assert_eq!(
12399            options,
12400            vec![
12401                "ci.skip".to_string(),
12402                "deploy target=staging".to_string(),
12403                String::new(),
12404            ]
12405        );
12406        assert_eq!(
12407            encode_receive_pack_push_options(&options).expect("test operation should succeed"),
12408            frames
12409        );
12410        assert_eq!(
12411            parse_receive_pack_push_options(&[PktLineFrame::Flush])
12412                .expect("test operation should succeed"),
12413            Vec::<String>::new()
12414        );
12415    }
12416
12417    #[test]
12418    fn receive_pack_push_options_streams_round_trip() {
12419        let options = vec!["ci.skip".to_string(), "reviewer=alice".to_string()];
12420        let mut encoded = Vec::new();
12421        write_receive_pack_push_options(&mut encoded, &options)
12422            .expect("test operation should succeed");
12423        encoded.extend_from_slice(b"PACK");
12424
12425        let mut input = encoded.as_slice();
12426        assert_eq!(
12427            read_receive_pack_push_options(&mut input).expect("test operation should succeed"),
12428            options
12429        );
12430        assert_eq!(input, b"PACK");
12431    }
12432
12433    #[test]
12434    fn receive_pack_push_options_reject_malformed_streams() {
12435        assert!(
12436            parse_receive_pack_push_options(&[PktLineFrame::Data(b"ci.skip\n".to_vec())]).is_err()
12437        );
12438        assert!(
12439            parse_receive_pack_push_options(&[PktLineFrame::Delimiter, PktLineFrame::Flush])
12440                .is_err()
12441        );
12442        assert!(
12443            parse_receive_pack_push_options(&[
12444                PktLineFrame::Data(b"ci.skip\n".to_vec()),
12445                PktLineFrame::Flush,
12446                PktLineFrame::Data(b"after\n".to_vec()),
12447            ])
12448            .is_err()
12449        );
12450        assert!(
12451            parse_receive_pack_push_options(&[
12452                PktLineFrame::Data(b"bad\0option\n".to_vec()),
12453                PktLineFrame::Flush,
12454            ])
12455            .is_err()
12456        );
12457        assert!(encode_receive_pack_push_options(&["bad\noption".to_string()]).is_err());
12458    }
12459
12460    #[test]
12461    fn protocol_v2_advertisement_parses_version_and_capabilities() {
12462        let frames = parse_pkt_line_stream(
12463            b"000eversion 2\n0015agent=git/2.54.0\n0013ls-refs=unborn\n0027fetch=shallow wait-for-done filter\n0012server-option\n0000",
12464        )
12465        .expect("test operation should succeed");
12466        let handshake =
12467            parse_protocol_v2_advertisement(&frames).expect("test operation should succeed");
12468        assert_eq!(handshake.protocol, ProtocolVersion::V2);
12469        assert_eq!(
12470            handshake.capabilities,
12471            vec![
12472                Capability {
12473                    name: "agent".into(),
12474                    value: Some("git/2.54.0".into()),
12475                },
12476                Capability {
12477                    name: "ls-refs".into(),
12478                    value: Some("unborn".into()),
12479                },
12480                Capability {
12481                    name: "fetch".into(),
12482                    value: Some("shallow wait-for-done filter".into()),
12483                },
12484                Capability {
12485                    name: "server-option".into(),
12486                    value: None,
12487                },
12488            ]
12489        );
12490        assert_eq!(
12491            encode_protocol_v2_advertisement(&handshake).expect("test operation should succeed"),
12492            frames
12493        );
12494    }
12495
12496    #[test]
12497    fn protocol_v2_advertisement_reads_until_flush() {
12498        let mut input = b"000eversion 2\n0013ls-refs=unborn\n0000next-session".as_slice();
12499        let handshake =
12500            read_protocol_v2_advertisement(&mut input).expect("test operation should succeed");
12501        assert_eq!(handshake.protocol, ProtocolVersion::V2);
12502        assert_eq!(
12503            handshake.capabilities,
12504            vec![Capability {
12505                name: "ls-refs".into(),
12506                value: Some("unborn".into()),
12507            }]
12508        );
12509        assert_eq!(input, b"next-session");
12510    }
12511
12512    #[test]
12513    fn protocol_v2_advertisement_writes_stream() {
12514        let handshake = TransportHandshake {
12515            protocol: ProtocolVersion::V2,
12516            capabilities: vec![
12517                Capability {
12518                    name: "agent".into(),
12519                    value: Some("sley/0".into()),
12520                },
12521                Capability {
12522                    name: "fetch".into(),
12523                    value: Some("shallow filter".into()),
12524                },
12525            ],
12526        };
12527        let mut encoded = Vec::new();
12528        write_protocol_v2_advertisement(&mut encoded, &handshake)
12529            .expect("test operation should succeed");
12530        let mut input = encoded.as_slice();
12531        assert_eq!(
12532            read_protocol_v2_advertisement(&mut input).expect("test operation should succeed"),
12533            handshake
12534        );
12535        assert!(input.is_empty());
12536        assert!(
12537            encode_protocol_v2_advertisement(&TransportHandshake {
12538                protocol: ProtocolVersion::V1,
12539                capabilities: Vec::new(),
12540            })
12541            .is_err()
12542        );
12543    }
12544
12545    #[test]
12546    fn protocol_v2_advertisement_rejects_malformed_sequences() {
12547        assert!(parse_protocol_v2_advertisement(&[]).is_err());
12548        assert!(
12549            parse_protocol_v2_advertisement(&[
12550                PktLineFrame::Data(b"version 1\n".to_vec()),
12551                PktLineFrame::Flush,
12552            ])
12553            .is_err()
12554        );
12555        assert!(
12556            parse_protocol_v2_advertisement(&[PktLineFrame::Data(b"version 2\n".to_vec())])
12557                .is_err()
12558        );
12559        assert!(
12560            parse_protocol_v2_advertisement(&[
12561                PktLineFrame::Data(b"version 2\n".to_vec()),
12562                PktLineFrame::Delimiter,
12563            ])
12564            .is_err()
12565        );
12566        assert!(
12567            parse_protocol_v2_advertisement(&[
12568                PktLineFrame::Data(b"version 2\n".to_vec()),
12569                PktLineFrame::Data(b"fetch=\n".to_vec()),
12570                PktLineFrame::Flush,
12571            ])
12572            .is_err()
12573        );
12574    }
12575
12576    #[test]
12577    fn protocol_v2_command_request_parses_and_encodes_sections() {
12578        let frames = parse_pkt_line_stream(
12579            b"0014command=ls-refs\n0011agent=sley/0\n0017object-format=sha1\n00010009peel\n000csymrefs\n001bref-prefix refs/heads/\n0000",
12580        )
12581        .expect("test operation should succeed");
12582        let request =
12583            parse_protocol_v2_command_request(&frames).expect("test operation should succeed");
12584        assert_eq!(
12585            request,
12586            ProtocolV2CommandRequest {
12587                command: "ls-refs".into(),
12588                capabilities: vec![
12589                    Capability {
12590                        name: "agent".into(),
12591                        value: Some("sley/0".into()),
12592                    },
12593                    Capability {
12594                        name: "object-format".into(),
12595                        value: Some("sha1".into()),
12596                    },
12597                ],
12598                arguments: vec![
12599                    b"peel".to_vec(),
12600                    b"symrefs".to_vec(),
12601                    b"ref-prefix refs/heads/".to_vec(),
12602                ],
12603            }
12604        );
12605        assert_eq!(
12606            encode_protocol_v2_command_request(&request).expect("test operation should succeed"),
12607            frames
12608        );
12609    }
12610
12611    #[test]
12612    fn protocol_v2_command_request_allows_no_argument_section() {
12613        let frames = parse_pkt_line_stream(b"0012command=fetch\n0000")
12614            .expect("test operation should succeed");
12615        let request =
12616            parse_protocol_v2_command_request(&frames).expect("test operation should succeed");
12617        assert_eq!(
12618            request,
12619            ProtocolV2CommandRequest {
12620                command: "fetch".into(),
12621                capabilities: Vec::new(),
12622                arguments: Vec::new(),
12623            }
12624        );
12625        assert_eq!(
12626            encode_protocol_v2_command_request(&request).expect("test operation should succeed"),
12627            frames
12628        );
12629    }
12630
12631    #[test]
12632    fn protocol_v2_request_parses_commands_and_empty_done() {
12633        let frames = parse_pkt_line_stream(b"0012command=fetch\n0000")
12634            .expect("test operation should succeed");
12635        let command = ProtocolV2CommandRequest {
12636            command: "fetch".into(),
12637            capabilities: Vec::new(),
12638            arguments: Vec::new(),
12639        };
12640        assert_eq!(
12641            parse_protocol_v2_request(&frames).expect("test operation should succeed"),
12642            ProtocolV2Request::Command(command.clone())
12643        );
12644        assert_eq!(
12645            encode_protocol_v2_request(&ProtocolV2Request::Command(command))
12646                .expect("test operation should succeed"),
12647            frames
12648        );
12649
12650        assert_eq!(
12651            parse_protocol_v2_request(&[PktLineFrame::Flush])
12652                .expect("test operation should succeed"),
12653            ProtocolV2Request::Done
12654        );
12655        assert_eq!(
12656            encode_protocol_v2_request(&ProtocolV2Request::Done)
12657                .expect("test operation should succeed"),
12658            vec![PktLineFrame::Flush]
12659        );
12660    }
12661
12662    #[test]
12663    fn protocol_v2_request_streams_empty_done() {
12664        let mut encoded = Vec::new();
12665        write_protocol_v2_request(&mut encoded, &ProtocolV2Request::Done)
12666            .expect("test operation should succeed");
12667        encoded.extend_from_slice(b"tail");
12668
12669        let mut input = encoded.as_slice();
12670        assert_eq!(
12671            read_protocol_v2_request(&mut input).expect("test operation should succeed"),
12672            ProtocolV2Request::Done
12673        );
12674        assert_eq!(input, b"tail");
12675        let mut command_input = encoded.as_slice();
12676        assert!(read_protocol_v2_command_request(&mut command_input).is_err());
12677    }
12678
12679    #[test]
12680    fn protocol_v2_command_request_streams_round_trip() {
12681        let request = ProtocolV2CommandRequest {
12682            command: "ls-refs".into(),
12683            capabilities: vec![Capability {
12684                name: "agent".into(),
12685                value: Some("sley/0".into()),
12686            }],
12687            arguments: vec![b"peel".to_vec(), b"symrefs".to_vec()],
12688        };
12689        let mut encoded = Vec::new();
12690        write_protocol_v2_command_request(&mut encoded, &request)
12691            .expect("test operation should succeed");
12692        encoded.extend_from_slice(b"tail");
12693
12694        let mut input = encoded.as_slice();
12695        assert_eq!(
12696            read_protocol_v2_command_request(&mut input).expect("test operation should succeed"),
12697            request
12698        );
12699        assert_eq!(input, b"tail");
12700    }
12701
12702    #[test]
12703    fn protocol_v2_command_request_rejects_malformed_sequences() {
12704        assert!(parse_protocol_v2_command_request(&[]).is_err());
12705        assert!(
12706            parse_protocol_v2_command_request(&[
12707                PktLineFrame::Data(b"agent=sley/0\n".to_vec()),
12708                PktLineFrame::Flush,
12709            ])
12710            .is_err()
12711        );
12712        assert!(
12713            parse_protocol_v2_command_request(&[
12714                PktLineFrame::Data(b"command=ls-refs\n".to_vec()),
12715                PktLineFrame::Delimiter,
12716                PktLineFrame::Delimiter,
12717                PktLineFrame::Flush,
12718            ])
12719            .is_err()
12720        );
12721        assert!(
12722            parse_protocol_v2_command_request(&[
12723                PktLineFrame::Data(b"command=ls-refs\n".to_vec()),
12724                PktLineFrame::Delimiter,
12725                PktLineFrame::Data(b"\n".to_vec()),
12726                PktLineFrame::Flush,
12727            ])
12728            .is_err()
12729        );
12730        assert!(
12731            encode_protocol_v2_command_request(&ProtocolV2CommandRequest {
12732                command: "bad command".into(),
12733                capabilities: Vec::new(),
12734                arguments: Vec::new(),
12735            })
12736            .is_err()
12737        );
12738    }
12739
12740    #[test]
12741    fn protocol_v2_ls_refs_request_parses_and_encodes_arguments() {
12742        let command = ProtocolV2CommandRequest {
12743            command: "ls-refs".into(),
12744            capabilities: Vec::new(),
12745            arguments: vec![
12746                b"peel".to_vec(),
12747                b"symrefs".to_vec(),
12748                b"unborn".to_vec(),
12749                b"ref-prefix HEAD".to_vec(),
12750                b"ref-prefix refs/heads/".to_vec(),
12751            ],
12752        };
12753        let request = ProtocolV2LsRefsRequest::from_command_request(&command)
12754            .expect("test operation should succeed");
12755        assert_eq!(
12756            request,
12757            ProtocolV2LsRefsRequest {
12758                peel: true,
12759                symrefs: true,
12760                unborn: true,
12761                ref_prefixes: vec!["HEAD".into(), "refs/heads/".into()],
12762            }
12763        );
12764        assert_eq!(
12765            request
12766                .to_command_request()
12767                .expect("test operation should succeed"),
12768            command
12769        );
12770        assert!(
12771            ProtocolV2LsRefsRequest::from_command_request(&ProtocolV2CommandRequest {
12772                command: "fetch".into(),
12773                capabilities: Vec::new(),
12774                arguments: Vec::new(),
12775            })
12776            .is_err()
12777        );
12778        assert!(
12779            ProtocolV2LsRefsRequest::from_command_request(&ProtocolV2CommandRequest {
12780                command: "ls-refs".into(),
12781                capabilities: Vec::new(),
12782                arguments: vec![b"ref-prefix ".to_vec()],
12783            })
12784            .is_err()
12785        );
12786    }
12787
12788    #[test]
12789    fn protocol_v2_ls_refs_request_streams_round_trip() {
12790        let request = ProtocolV2LsRefsRequest {
12791            peel: true,
12792            symrefs: true,
12793            unborn: false,
12794            ref_prefixes: vec!["HEAD".into(), "refs/tags/".into()],
12795        };
12796        let mut encoded = Vec::new();
12797        write_protocol_v2_ls_refs_request(&mut encoded, &request)
12798            .expect("test operation should succeed");
12799        encoded.extend_from_slice(b"tail");
12800
12801        let mut input = encoded.as_slice();
12802        assert_eq!(
12803            read_protocol_v2_ls_refs_request(&mut input).expect("test operation should succeed"),
12804            request
12805        );
12806        assert_eq!(input, b"tail");
12807    }
12808
12809    #[test]
12810    fn protocol_v2_ls_refs_response_parses_and_encodes_records() {
12811        let oid = ObjectId::from_hex(
12812            ObjectFormat::Sha1,
12813            "1111111111111111111111111111111111111111",
12814        )
12815        .expect("test operation should succeed");
12816        let peeled = ObjectId::from_hex(
12817            ObjectFormat::Sha1,
12818            "2222222222222222222222222222222222222222",
12819        )
12820        .expect("test operation should succeed");
12821        let frames = vec![
12822            PktLineFrame::Data(
12823                b"1111111111111111111111111111111111111111 refs/tags/v1 peeled:2222222222222222222222222222222222222222 symref-target:refs/heads/main custom\n"
12824                    .to_vec(),
12825            ),
12826            PktLineFrame::Data(b"unborn HEAD symref-target:refs/heads/main\n".to_vec()),
12827            PktLineFrame::Flush,
12828        ];
12829        let records = parse_protocol_v2_ls_refs_response(ObjectFormat::Sha1, &frames)
12830            .expect("test operation should succeed");
12831        assert_eq!(
12832            records,
12833            vec![
12834                ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
12835                    oid,
12836                    name: "refs/tags/v1".into(),
12837                    peeled: Some(peeled),
12838                    symref_target: Some("refs/heads/main".into()),
12839                    attributes: vec!["custom".into()],
12840                }),
12841                ProtocolV2LsRefsRecord::Unborn {
12842                    name: "HEAD".into(),
12843                    symref_target: Some("refs/heads/main".into()),
12844                    attributes: Vec::new(),
12845                },
12846            ]
12847        );
12848        assert_eq!(
12849            encode_protocol_v2_ls_refs_response(&records).expect("test operation should succeed"),
12850            frames
12851        );
12852    }
12853
12854    #[test]
12855    fn protocol_v2_ls_refs_response_streams_round_trip() {
12856        let oid = ObjectId::from_hex(
12857            ObjectFormat::Sha1,
12858            "1111111111111111111111111111111111111111",
12859        )
12860        .expect("test operation should succeed");
12861        let records = vec![ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
12862            oid,
12863            name: "refs/heads/main".into(),
12864            peeled: None,
12865            symref_target: Some("refs/heads/trunk".into()),
12866            attributes: vec!["custom".into()],
12867        })];
12868        let mut encoded = Vec::new();
12869        write_protocol_v2_ls_refs_response(&mut encoded, &records)
12870            .expect("test operation should succeed");
12871        encoded.extend_from_slice(b"tail");
12872
12873        let mut input = encoded.as_slice();
12874        assert_eq!(
12875            read_protocol_v2_ls_refs_response(ObjectFormat::Sha1, &mut input)
12876                .expect("test operation should succeed"),
12877            records
12878        );
12879        assert_eq!(input, b"tail");
12880    }
12881
12882    #[test]
12883    fn protocol_v2_ls_refs_response_reads_stateless_response_end() {
12884        let oid = ObjectId::from_hex(
12885            ObjectFormat::Sha1,
12886            "1111111111111111111111111111111111111111",
12887        )
12888        .expect("test operation should succeed");
12889        let records = vec![ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
12890            oid,
12891            name: "refs/heads/main".into(),
12892            peeled: None,
12893            symref_target: None,
12894            attributes: Vec::new(),
12895        })];
12896        let mut encoded = Vec::new();
12897        write_protocol_v2_ls_refs_response_with_response_end(&mut encoded, &records)
12898            .expect("test operation should succeed");
12899        encoded.extend_from_slice(b"tail");
12900
12901        let mut input = encoded.as_slice();
12902        assert_eq!(
12903            read_protocol_v2_ls_refs_response_until_response_end(ObjectFormat::Sha1, &mut input)
12904                .expect("test operation should succeed"),
12905            records
12906        );
12907        assert_eq!(input, b"tail");
12908        assert!(
12909            parse_protocol_v2_ls_refs_response(
12910                ObjectFormat::Sha1,
12911                &[
12912                    PktLineFrame::Data(
12913                        b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec()
12914                    ),
12915                    PktLineFrame::ResponseEnd
12916                ],
12917            )
12918            .is_err()
12919        );
12920    }
12921
12922    #[test]
12923    fn protocol_v2_ls_refs_exchange_writes_request_and_reads_response() {
12924        let oid = ObjectId::from_hex(
12925            ObjectFormat::Sha1,
12926            "1111111111111111111111111111111111111111",
12927        )
12928        .expect("test operation should succeed");
12929        let request = ProtocolV2LsRefsRequest {
12930            peel: true,
12931            symrefs: true,
12932            unborn: false,
12933            ref_prefixes: vec!["refs/heads/".into()],
12934        };
12935        let records = vec![ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
12936            oid,
12937            name: "refs/heads/main".into(),
12938            peeled: None,
12939            symref_target: None,
12940            attributes: Vec::new(),
12941        })];
12942        let mut response = Vec::new();
12943        write_protocol_v2_ls_refs_response(&mut response, &records)
12944            .expect("test operation should succeed");
12945
12946        let mut input = response.as_slice();
12947        let mut output = Vec::new();
12948        assert_eq!(
12949            exchange_protocol_v2_ls_refs(ObjectFormat::Sha1, &mut input, &mut output, &request)
12950                .expect("test operation should succeed"),
12951            records
12952        );
12953        assert!(input.is_empty());
12954        let mut output_read = output.as_slice();
12955        assert_eq!(
12956            read_protocol_v2_ls_refs_request(&mut output_read)
12957                .expect("test operation should succeed"),
12958            request
12959        );
12960    }
12961
12962    #[test]
12963    fn protocol_v2_ls_refs_response_rejects_malformed_records() {
12964        assert!(
12965            parse_protocol_v2_ls_refs_response(
12966                ObjectFormat::Sha1,
12967                &[PktLineFrame::Data(
12968                    b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec()
12969                )],
12970            )
12971            .is_err()
12972        );
12973        assert!(
12974            parse_protocol_v2_ls_refs_response(
12975                ObjectFormat::Sha1,
12976                &[
12977                    PktLineFrame::Data(
12978                        b"1111111111111111111111111111111111111111 refs/heads/main peeled:2222222222222222222222222222222222222222 peeled:3333333333333333333333333333333333333333\n"
12979                            .to_vec()
12980                    ),
12981                    PktLineFrame::Flush,
12982                ],
12983            )
12984            .is_err()
12985        );
12986        assert!(
12987            parse_protocol_v2_ls_refs_response(
12988                ObjectFormat::Sha1,
12989                &[
12990                    PktLineFrame::Data(
12991                        b"unborn HEAD peeled:2222222222222222222222222222222222222222\n".to_vec()
12992                    ),
12993                    PktLineFrame::Flush,
12994                ],
12995            )
12996            .is_err()
12997        );
12998        assert!(
12999            encode_protocol_v2_ls_refs_response(&[ProtocolV2LsRefsRecord::Ref(
13000                ProtocolV2LsRefsRef {
13001                    oid: ObjectId::from_hex(
13002                        ObjectFormat::Sha1,
13003                        "1111111111111111111111111111111111111111",
13004                    )
13005                    .expect("test operation should succeed"),
13006                    name: "refs/heads/main".into(),
13007                    peeled: None,
13008                    symref_target: None,
13009                    attributes: vec!["peeled:2222222222222222222222222222222222222222".into()],
13010                }
13011            )])
13012            .is_err()
13013        );
13014    }
13015
13016    #[test]
13017    fn protocol_v2_fetch_request_parses_and_encodes_arguments() {
13018        let want = ObjectId::from_hex(
13019            ObjectFormat::Sha1,
13020            "1111111111111111111111111111111111111111",
13021        )
13022        .expect("test operation should succeed");
13023        let have = ObjectId::from_hex(
13024            ObjectFormat::Sha1,
13025            "2222222222222222222222222222222222222222",
13026        )
13027        .expect("test operation should succeed");
13028        let shallow = ObjectId::from_hex(
13029            ObjectFormat::Sha1,
13030            "3333333333333333333333333333333333333333",
13031        )
13032        .expect("test operation should succeed");
13033        let command = ProtocolV2CommandRequest {
13034            command: "fetch".into(),
13035            capabilities: Vec::new(),
13036            arguments: vec![
13037                b"want 1111111111111111111111111111111111111111".to_vec(),
13038                b"want-ref refs/heads/main".to_vec(),
13039                b"have 2222222222222222222222222222222222222222".to_vec(),
13040                b"shallow 3333333333333333333333333333333333333333".to_vec(),
13041                b"deepen 10".to_vec(),
13042                b"deepen-since 123456789".to_vec(),
13043                b"deepen-not refs/tags/v1".to_vec(),
13044                b"deepen-relative".to_vec(),
13045                b"filter blob:none".to_vec(),
13046                b"packfile-uris http,https".to_vec(),
13047                b"thin-pack".to_vec(),
13048                b"no-progress".to_vec(),
13049                b"include-tag".to_vec(),
13050                b"ofs-delta".to_vec(),
13051                b"sideband-all".to_vec(),
13052                b"wait-for-done".to_vec(),
13053                b"done".to_vec(),
13054            ],
13055        };
13056        let request = ProtocolV2FetchRequest::from_command_request(ObjectFormat::Sha1, &command)
13057            .expect("test operation should succeed");
13058        assert_eq!(
13059            request,
13060            ProtocolV2FetchRequest {
13061                wants: vec![want],
13062                want_refs: vec!["refs/heads/main".into()],
13063                haves: vec![have],
13064                shallow: vec![shallow],
13065                deepen: Some(10),
13066                deepen_since: Some(123456789),
13067                deepen_not: vec!["refs/tags/v1".into()],
13068                deepen_relative: true,
13069                filter: Some("blob:none".into()),
13070                packfile_uris: Some("http,https".into()),
13071                thin_pack: true,
13072                no_progress: true,
13073                include_tag: true,
13074                ofs_delta: true,
13075                sideband_all: true,
13076                wait_for_done: true,
13077                done: true,
13078            }
13079        );
13080        assert_eq!(
13081            request
13082                .to_command_request()
13083                .expect("test operation should succeed"),
13084            command
13085        );
13086    }
13087
13088    #[test]
13089    fn protocol_v2_fetch_request_rejects_malformed_arguments() {
13090        assert!(
13091            ProtocolV2FetchRequest::from_command_request(
13092                ObjectFormat::Sha1,
13093                &ProtocolV2CommandRequest {
13094                    command: "ls-refs".into(),
13095                    capabilities: Vec::new(),
13096                    arguments: Vec::new(),
13097                },
13098            )
13099            .is_err()
13100        );
13101        assert!(
13102            ProtocolV2FetchRequest::from_command_request(
13103                ObjectFormat::Sha1,
13104                &ProtocolV2CommandRequest {
13105                    command: "fetch".into(),
13106                    capabilities: Vec::new(),
13107                    arguments: vec![b"want not-an-oid".to_vec()],
13108                },
13109            )
13110            .is_err()
13111        );
13112        assert!(
13113            ProtocolV2FetchRequest::from_command_request(
13114                ObjectFormat::Sha1,
13115                &ProtocolV2CommandRequest {
13116                    command: "fetch".into(),
13117                    capabilities: Vec::new(),
13118                    arguments: vec![b"deepen 0".to_vec()],
13119                },
13120            )
13121            .is_err()
13122        );
13123        assert!(
13124            ProtocolV2FetchRequest::from_command_request(
13125                ObjectFormat::Sha1,
13126                &ProtocolV2CommandRequest {
13127                    command: "fetch".into(),
13128                    capabilities: Vec::new(),
13129                    arguments: vec![b"filter blob:none".to_vec(), b"filter tree:0".to_vec()],
13130                },
13131            )
13132            .is_err()
13133        );
13134        assert!(
13135            ProtocolV2FetchRequest {
13136                deepen: Some(0),
13137                ..ProtocolV2FetchRequest::default()
13138            }
13139            .to_command_request()
13140            .is_err()
13141        );
13142    }
13143
13144    #[test]
13145    fn protocol_v2_fetch_request_streams_round_trip() {
13146        let want = ObjectId::from_hex(
13147            ObjectFormat::Sha1,
13148            "1111111111111111111111111111111111111111",
13149        )
13150        .expect("test operation should succeed");
13151        let have = ObjectId::from_hex(
13152            ObjectFormat::Sha1,
13153            "2222222222222222222222222222222222222222",
13154        )
13155        .expect("test operation should succeed");
13156        let request = ProtocolV2FetchRequest {
13157            wants: vec![want],
13158            haves: vec![have],
13159            deepen: Some(5),
13160            filter: Some("blob:none".into()),
13161            thin_pack: true,
13162            done: true,
13163            ..ProtocolV2FetchRequest::default()
13164        };
13165        let mut encoded = Vec::new();
13166        write_protocol_v2_fetch_request(&mut encoded, &request)
13167            .expect("test operation should succeed");
13168        encoded.extend_from_slice(b"tail");
13169
13170        let mut input = encoded.as_slice();
13171        assert_eq!(
13172            read_protocol_v2_fetch_request(ObjectFormat::Sha1, &mut input)
13173                .expect("test operation should succeed"),
13174            request
13175        );
13176        assert_eq!(input, b"tail");
13177    }
13178
13179    #[test]
13180    fn protocol_v2_fetch_response_parses_and_encodes_sections() {
13181        let ack = ObjectId::from_hex(
13182            ObjectFormat::Sha1,
13183            "1111111111111111111111111111111111111111",
13184        )
13185        .expect("test operation should succeed");
13186        let shallow = ObjectId::from_hex(
13187            ObjectFormat::Sha1,
13188            "2222222222222222222222222222222222222222",
13189        )
13190        .expect("test operation should succeed");
13191        let wanted = ObjectId::from_hex(
13192            ObjectFormat::Sha1,
13193            "3333333333333333333333333333333333333333",
13194        )
13195        .expect("test operation should succeed");
13196        let pack_hash = ObjectId::from_hex(
13197            ObjectFormat::Sha1,
13198            "4444444444444444444444444444444444444444",
13199        )
13200        .expect("test operation should succeed");
13201        let frames = vec![
13202            PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13203            PktLineFrame::Data(b"ACK 1111111111111111111111111111111111111111\n".to_vec()),
13204            PktLineFrame::Data(b"ready\n".to_vec()),
13205            PktLineFrame::Delimiter,
13206            PktLineFrame::Data(b"shallow-info\n".to_vec()),
13207            PktLineFrame::Data(b"shallow 2222222222222222222222222222222222222222\n".to_vec()),
13208            PktLineFrame::Delimiter,
13209            PktLineFrame::Data(b"wanted-refs\n".to_vec()),
13210            PktLineFrame::Data(
13211                b"3333333333333333333333333333333333333333 refs/heads/main\n".to_vec(),
13212            ),
13213            PktLineFrame::Delimiter,
13214            PktLineFrame::Data(b"packfile-uris\n".to_vec()),
13215            PktLineFrame::Data(
13216                b"4444444444444444444444444444444444444444 https://example.invalid/pack-a.pack\n"
13217                    .to_vec(),
13218            ),
13219            PktLineFrame::Delimiter,
13220            PktLineFrame::Data(b"packfile\n".to_vec()),
13221            PktLineFrame::Data(b"\x01PACK bytes".to_vec()),
13222            PktLineFrame::Flush,
13223        ];
13224        let sections = parse_protocol_v2_fetch_response(ObjectFormat::Sha1, &frames)
13225            .expect("test operation should succeed");
13226        assert_eq!(
13227            sections,
13228            vec![
13229                ProtocolV2FetchResponseSection::Acknowledgments(vec![
13230                    ProtocolV2FetchAcknowledgment::Ack(ack),
13231                    ProtocolV2FetchAcknowledgment::Ready,
13232                ]),
13233                ProtocolV2FetchResponseSection::ShallowInfo(vec![
13234                    ProtocolV2FetchShallowInfo::Shallow(shallow)
13235                ]),
13236                ProtocolV2FetchResponseSection::WantedRefs(vec![ProtocolV2FetchWantedRef {
13237                    oid: wanted,
13238                    name: "refs/heads/main".into(),
13239                }]),
13240                ProtocolV2FetchResponseSection::PackfileUris(vec![ProtocolV2FetchPackfileUri {
13241                    pack_hash,
13242                    uri: "https://example.invalid/pack-a.pack".into(),
13243                }]),
13244                ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK bytes".to_vec()]),
13245            ]
13246        );
13247        assert_eq!(
13248            encode_protocol_v2_fetch_response(&sections).expect("test operation should succeed"),
13249            frames
13250        );
13251    }
13252
13253    #[test]
13254    fn protocol_v2_fetch_response_preserves_unknown_sections() {
13255        let frames = vec![
13256            PktLineFrame::Data(b"server-feature\n".to_vec()),
13257            PktLineFrame::Data(b"opaque line\n".to_vec()),
13258            PktLineFrame::Flush,
13259        ];
13260        let sections = parse_protocol_v2_fetch_response(ObjectFormat::Sha1, &frames)
13261            .expect("test operation should succeed");
13262        assert_eq!(
13263            sections,
13264            vec![ProtocolV2FetchResponseSection::Unknown {
13265                name: "server-feature".into(),
13266                lines: vec![b"opaque line\n".to_vec()],
13267            }]
13268        );
13269        assert_eq!(
13270            encode_protocol_v2_fetch_response(&sections).expect("test operation should succeed"),
13271            frames
13272        );
13273    }
13274
13275    #[test]
13276    fn protocol_v2_fetch_response_streams_round_trip() {
13277        let ack = ObjectId::from_hex(
13278            ObjectFormat::Sha1,
13279            "1111111111111111111111111111111111111111",
13280        )
13281        .expect("test operation should succeed");
13282        let sections = vec![
13283            ProtocolV2FetchResponseSection::Acknowledgments(vec![
13284                ProtocolV2FetchAcknowledgment::Ack(ack),
13285                ProtocolV2FetchAcknowledgment::Ready,
13286            ]),
13287            ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK bytes".to_vec()]),
13288        ];
13289        let mut encoded = Vec::new();
13290        write_protocol_v2_fetch_response(&mut encoded, &sections)
13291            .expect("test operation should succeed");
13292        encoded.extend_from_slice(b"tail");
13293
13294        let mut input = encoded.as_slice();
13295        assert_eq!(
13296            read_protocol_v2_fetch_response(ObjectFormat::Sha1, &mut input)
13297                .expect("test operation should succeed"),
13298            sections
13299        );
13300        assert_eq!(input, b"tail");
13301    }
13302
13303    #[test]
13304    fn protocol_v2_fetch_sideband_all_response_parses_sections_and_progress() {
13305        let frames = vec![
13306            PktLineFrame::Data(
13307                encode_sideband_packet(&SideBandPacket {
13308                    channel: SideBandChannel::Data,
13309                    data: b"acknowledgments\n".to_vec(),
13310                })
13311                .expect("test operation should succeed"),
13312            ),
13313            PktLineFrame::Data(
13314                encode_sideband_packet(&SideBandPacket {
13315                    channel: SideBandChannel::Data,
13316                    data: b"NAK\n".to_vec(),
13317                })
13318                .expect("test operation should succeed"),
13319            ),
13320            PktLineFrame::Data(
13321                encode_sideband_packet(&SideBandPacket {
13322                    channel: SideBandChannel::Progress,
13323                    data: b"keepalive\n".to_vec(),
13324                })
13325                .expect("test operation should succeed"),
13326            ),
13327            PktLineFrame::Delimiter,
13328            PktLineFrame::Data(
13329                encode_sideband_packet(&SideBandPacket {
13330                    channel: SideBandChannel::Data,
13331                    data: b"packfile\n".to_vec(),
13332                })
13333                .expect("test operation should succeed"),
13334            ),
13335            PktLineFrame::Data(b"\x01PACK".to_vec()),
13336            PktLineFrame::Data(b"\x02counting objects\n".to_vec()),
13337            PktLineFrame::Flush,
13338        ];
13339
13340        let response = parse_protocol_v2_fetch_sideband_all_response(ObjectFormat::Sha1, &frames)
13341            .expect("test operation should succeed");
13342        assert_eq!(
13343            response,
13344            ProtocolV2FetchSidebandAllResponse {
13345                sections: vec![
13346                    ProtocolV2FetchResponseSection::Acknowledgments(vec![
13347                        ProtocolV2FetchAcknowledgment::Nak
13348                    ]),
13349                    ProtocolV2FetchResponseSection::Packfile(vec![
13350                        b"\x01PACK".to_vec(),
13351                        b"\x02counting objects\n".to_vec(),
13352                    ]),
13353                ],
13354                progress: vec![b"keepalive\n".to_vec()],
13355            }
13356        );
13357        assert_eq!(
13358            demux_protocol_v2_fetch_packfile(&response.sections)
13359                .expect("test operation should succeed"),
13360            Some(SideBandDemux {
13361                data: b"PACK".to_vec(),
13362                progress: vec![b"counting objects\n".to_vec()],
13363            })
13364        );
13365    }
13366
13367    #[test]
13368    fn protocol_v2_fetch_sideband_all_response_streams_round_trip() {
13369        let sections = vec![
13370            ProtocolV2FetchResponseSection::Acknowledgments(vec![
13371                ProtocolV2FetchAcknowledgment::Nak,
13372            ]),
13373            ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK bytes".to_vec()]),
13374        ];
13375        let mut encoded = Vec::new();
13376        write_protocol_v2_fetch_sideband_all_response(&mut encoded, &sections)
13377            .expect("test operation should succeed");
13378        encoded.extend_from_slice(b"tail");
13379
13380        let mut input = encoded.as_slice();
13381        assert_eq!(
13382            read_protocol_v2_fetch_sideband_all_response(ObjectFormat::Sha1, &mut input)
13383                .expect("test operation should succeed"),
13384            ProtocolV2FetchSidebandAllResponse {
13385                sections: sections.clone(),
13386                progress: Vec::new(),
13387            }
13388        );
13389        assert_eq!(input, b"tail");
13390
13391        let mut encoded = Vec::new();
13392        write_protocol_v2_fetch_sideband_all_response_with_response_end(&mut encoded, &sections)
13393            .expect("test operation should succeed");
13394        encoded.extend_from_slice(b"tail");
13395
13396        let mut input = encoded.as_slice();
13397        assert_eq!(
13398            read_protocol_v2_fetch_sideband_all_response_until_response_end(
13399                ObjectFormat::Sha1,
13400                &mut input,
13401            )
13402            .expect("test operation should succeed")
13403            .sections,
13404            sections
13405        );
13406        assert_eq!(input, b"tail");
13407    }
13408
13409    #[test]
13410    fn protocol_v2_fetch_sideband_all_response_rejects_malformed_sideband() {
13411        assert!(
13412            parse_protocol_v2_fetch_sideband_all_response(
13413                ObjectFormat::Sha1,
13414                &[
13415                    PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13416                    PktLineFrame::Flush,
13417                ],
13418            )
13419            .is_err()
13420        );
13421        assert!(
13422            parse_protocol_v2_fetch_sideband_all_response(
13423                ObjectFormat::Sha1,
13424                &[
13425                    PktLineFrame::Data(
13426                        encode_sideband_packet(&SideBandPacket {
13427                            channel: SideBandChannel::Fatal,
13428                            data: b"remote died\n".to_vec(),
13429                        })
13430                        .expect("test operation should succeed"),
13431                    ),
13432                    PktLineFrame::Flush,
13433                ],
13434            )
13435            .is_err()
13436        );
13437    }
13438
13439    #[test]
13440    fn protocol_v2_object_info_response_parses_and_encodes_size_records() {
13441        let oid = ObjectId::from_hex(
13442            ObjectFormat::Sha1,
13443            "1111111111111111111111111111111111111111",
13444        )
13445        .expect("test operation should succeed");
13446        let frames = vec![
13447            PktLineFrame::Data(b"size\n".to_vec()),
13448            PktLineFrame::Data(b"1111111111111111111111111111111111111111 12345\n".to_vec()),
13449            PktLineFrame::Flush,
13450        ];
13451        let response = parse_protocol_v2_object_info_response(ObjectFormat::Sha1, &frames)
13452            .expect("test operation should succeed");
13453        assert_eq!(
13454            response,
13455            ProtocolV2ObjectInfoResponse {
13456                size: true,
13457                records: vec![ProtocolV2ObjectInfoRecord { oid, size: 12345 }],
13458            }
13459        );
13460        assert_eq!(
13461            encode_protocol_v2_object_info_response(&response)
13462                .expect("test operation should succeed"),
13463            frames
13464        );
13465    }
13466
13467    #[test]
13468    fn protocol_v2_object_info_response_streams_and_exchanges() {
13469        let request = ProtocolV2ObjectInfoRequest {
13470            size: true,
13471            oids: vec![
13472                ObjectId::from_hex(
13473                    ObjectFormat::Sha1,
13474                    "1111111111111111111111111111111111111111",
13475                )
13476                .expect("test operation should succeed"),
13477            ],
13478        };
13479        let response = ProtocolV2ObjectInfoResponse {
13480            size: true,
13481            records: vec![ProtocolV2ObjectInfoRecord {
13482                oid: request.oids[0].clone(),
13483                size: 7,
13484            }],
13485        };
13486
13487        let mut encoded = Vec::new();
13488        write_protocol_v2_object_info_response(&mut encoded, &response)
13489            .expect("test operation should succeed");
13490        encoded.extend_from_slice(b"tail");
13491        let mut input = encoded.as_slice();
13492        assert_eq!(
13493            read_protocol_v2_object_info_response(ObjectFormat::Sha1, &mut input)
13494                .expect("test operation should succeed"),
13495            response
13496        );
13497        assert_eq!(input, b"tail");
13498
13499        let mut response_bytes = Vec::new();
13500        write_protocol_v2_object_info_response(&mut response_bytes, &response)
13501            .expect("test operation should succeed");
13502        let mut input = response_bytes.as_slice();
13503        let mut output = Vec::new();
13504        assert_eq!(
13505            exchange_protocol_v2_object_info(
13506                ObjectFormat::Sha1,
13507                &mut input,
13508                &mut output,
13509                &request,
13510            )
13511            .expect("test operation should succeed"),
13512            response
13513        );
13514        assert!(input.is_empty());
13515        let mut output_read = output.as_slice();
13516        assert_eq!(
13517            read_protocol_v2_object_info_request(ObjectFormat::Sha1, &mut output_read)
13518                .expect("test operation should succeed"),
13519            request
13520        );
13521    }
13522
13523    #[test]
13524    fn protocol_v2_object_info_response_rejects_malformed_records() {
13525        assert!(parse_protocol_v2_object_info_response(ObjectFormat::Sha1, &[]).is_err());
13526        assert!(
13527            parse_protocol_v2_object_info_response(
13528                ObjectFormat::Sha1,
13529                &[PktLineFrame::Data(b"size\n".to_vec())],
13530            )
13531            .is_err()
13532        );
13533        assert!(
13534            parse_protocol_v2_object_info_response(
13535                ObjectFormat::Sha1,
13536                &[PktLineFrame::Data(b"type\n".to_vec()), PktLineFrame::Flush,],
13537            )
13538            .is_err()
13539        );
13540        assert!(
13541            parse_protocol_v2_object_info_response(
13542                ObjectFormat::Sha1,
13543                &[
13544                    PktLineFrame::Data(b"size\n".to_vec()),
13545                    PktLineFrame::Data(
13546                        b"1111111111111111111111111111111111111111 not-a-size\n".to_vec()
13547                    ),
13548                    PktLineFrame::Flush,
13549                ],
13550            )
13551            .is_err()
13552        );
13553        assert!(
13554            parse_protocol_v2_object_info_response(
13555                ObjectFormat::Sha1,
13556                &[
13557                    PktLineFrame::Data(b"size\n".to_vec()),
13558                    PktLineFrame::Delimiter,
13559                    PktLineFrame::Flush,
13560                ],
13561            )
13562            .is_err()
13563        );
13564        assert!(
13565            encode_protocol_v2_object_info_response(&ProtocolV2ObjectInfoResponse {
13566                size: false,
13567                records: Vec::new(),
13568            })
13569            .is_err()
13570        );
13571    }
13572
13573    #[test]
13574    fn protocol_v2_fetch_response_reads_stateless_response_end() {
13575        let sections = vec![ProtocolV2FetchResponseSection::Acknowledgments(vec![
13576            ProtocolV2FetchAcknowledgment::Nak,
13577        ])];
13578        let mut encoded = Vec::new();
13579        write_protocol_v2_fetch_response_with_response_end(&mut encoded, &sections)
13580            .expect("test operation should succeed");
13581        encoded.extend_from_slice(b"tail");
13582
13583        let mut input = encoded.as_slice();
13584        assert_eq!(
13585            read_protocol_v2_fetch_response_until_response_end(ObjectFormat::Sha1, &mut input)
13586                .expect("test operation should succeed"),
13587            sections
13588        );
13589        assert_eq!(input, b"tail");
13590        assert!(
13591            parse_protocol_v2_fetch_response(
13592                ObjectFormat::Sha1,
13593                &[
13594                    PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13595                    PktLineFrame::ResponseEnd,
13596                ],
13597            )
13598            .is_err()
13599        );
13600    }
13601
13602    #[test]
13603    fn protocol_v2_fetch_exchange_writes_request_and_reads_response() {
13604        let want = ObjectId::from_hex(
13605            ObjectFormat::Sha1,
13606            "1111111111111111111111111111111111111111",
13607        )
13608        .expect("test operation should succeed");
13609        let request = ProtocolV2FetchRequest {
13610            wants: vec![want],
13611            thin_pack: true,
13612            done: true,
13613            ..ProtocolV2FetchRequest::default()
13614        };
13615        let sections = vec![ProtocolV2FetchResponseSection::Acknowledgments(vec![
13616            ProtocolV2FetchAcknowledgment::Nak,
13617        ])];
13618        let mut response = Vec::new();
13619        write_protocol_v2_fetch_response(&mut response, &sections)
13620            .expect("test operation should succeed");
13621
13622        let mut input = response.as_slice();
13623        let mut output = Vec::new();
13624        assert_eq!(
13625            exchange_protocol_v2_fetch(ObjectFormat::Sha1, &mut input, &mut output, &request)
13626                .expect("test operation should succeed"),
13627            sections
13628        );
13629        assert!(input.is_empty());
13630        let mut output_read = output.as_slice();
13631        assert_eq!(
13632            read_protocol_v2_fetch_request(ObjectFormat::Sha1, &mut output_read)
13633                .expect("test operation should succeed"),
13634            request
13635        );
13636    }
13637
13638    #[test]
13639    fn protocol_v2_fetch_packfile_demuxes_sideband_section() {
13640        let sections = vec![
13641            ProtocolV2FetchResponseSection::Acknowledgments(vec![
13642                ProtocolV2FetchAcknowledgment::Nak,
13643            ]),
13644            ProtocolV2FetchResponseSection::Packfile(vec![
13645                b"\x01PACK".to_vec(),
13646                b"\x02counting objects\n".to_vec(),
13647                b"\x01 bytes".to_vec(),
13648                b"\x02done\n".to_vec(),
13649            ]),
13650        ];
13651
13652        assert_eq!(
13653            demux_protocol_v2_fetch_packfile(&sections).expect("test operation should succeed"),
13654            Some(SideBandDemux {
13655                data: b"PACK bytes".to_vec(),
13656                progress: vec![b"counting objects\n".to_vec(), b"done\n".to_vec()],
13657            })
13658        );
13659        assert_eq!(
13660            demux_protocol_v2_fetch_packfile(&[ProtocolV2FetchResponseSection::Acknowledgments(
13661                vec![ProtocolV2FetchAcknowledgment::Nak],
13662            )])
13663            .expect("test operation should succeed"),
13664            None
13665        );
13666    }
13667
13668    #[test]
13669    fn protocol_v2_fetch_packfile_demux_rejects_duplicate_or_bad_sideband() {
13670        assert!(
13671            demux_protocol_v2_fetch_packfile(&[
13672                ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK".to_vec()]),
13673                ProtocolV2FetchResponseSection::Packfile(vec![b"\x01more".to_vec()]),
13674            ])
13675            .is_err()
13676        );
13677        assert!(
13678            demux_protocol_v2_fetch_packfile(&[ProtocolV2FetchResponseSection::Packfile(vec![
13679                b"\x03remote died\n".to_vec()
13680            ])])
13681            .is_err()
13682        );
13683        assert!(
13684            demux_protocol_v2_fetch_packfile(&[ProtocolV2FetchResponseSection::Packfile(vec![
13685                b"\x04bad".to_vec()
13686            ])])
13687            .is_err()
13688        );
13689    }
13690
13691    #[test]
13692    fn protocol_v2_fetch_response_rejects_malformed_sections() {
13693        assert!(
13694            parse_protocol_v2_fetch_response(
13695                ObjectFormat::Sha1,
13696                &[PktLineFrame::Data(b"acknowledgments\n".to_vec())],
13697            )
13698            .is_err()
13699        );
13700        assert!(
13701            parse_protocol_v2_fetch_response(
13702                ObjectFormat::Sha1,
13703                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
13704            )
13705            .is_err()
13706        );
13707        assert!(
13708            parse_protocol_v2_fetch_response(
13709                ObjectFormat::Sha1,
13710                &[
13711                    PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13712                    PktLineFrame::Data(b"ACK not-an-oid\n".to_vec()),
13713                    PktLineFrame::Flush,
13714                ],
13715            )
13716            .is_err()
13717        );
13718        assert!(
13719            parse_protocol_v2_fetch_response(
13720                ObjectFormat::Sha1,
13721                &[
13722                    PktLineFrame::Data(b"packfile-uris\n".to_vec()),
13723                    PktLineFrame::Data(b"https://example.invalid/pack-a.pack\n".to_vec()),
13724                    PktLineFrame::Flush,
13725                ],
13726            )
13727            .is_err()
13728        );
13729        assert!(
13730            parse_protocol_v2_fetch_response(
13731                ObjectFormat::Sha1,
13732                &[
13733                    PktLineFrame::Data(b"packfile-uris\n".to_vec()),
13734                    PktLineFrame::Data(
13735                        b"not-a-hash https://example.invalid/pack-a.pack\n".to_vec()
13736                    ),
13737                    PktLineFrame::Flush,
13738                ],
13739            )
13740            .is_err()
13741        );
13742        assert!(
13743            encode_protocol_v2_fetch_response(&[ProtocolV2FetchResponseSection::WantedRefs(vec![
13744                ProtocolV2FetchWantedRef {
13745                    oid: ObjectId::from_hex(
13746                        ObjectFormat::Sha1,
13747                        "1111111111111111111111111111111111111111",
13748                    )
13749                    .expect("test operation should succeed"),
13750                    name: "bad ref".into(),
13751                }
13752            ])])
13753            .is_err()
13754        );
13755    }
13756
13757    #[test]
13758    fn protocol_v2_ls_refs_response_bridges_into_ref_advertisement_set() {
13759        let head = ObjectId::from_hex(
13760            ObjectFormat::Sha1,
13761            "1111111111111111111111111111111111111111",
13762        )
13763        .expect("test operation should succeed");
13764        let tag = ObjectId::from_hex(
13765            ObjectFormat::Sha1,
13766            "2222222222222222222222222222222222222222",
13767        )
13768        .expect("test operation should succeed");
13769        let tag_peeled = ObjectId::from_hex(
13770            ObjectFormat::Sha1,
13771            "3333333333333333333333333333333333333333",
13772        )
13773        .expect("test operation should succeed");
13774        let frames = vec![
13775            PktLineFrame::Data(
13776                b"1111111111111111111111111111111111111111 HEAD symref-target:refs/heads/main\n"
13777                    .to_vec(),
13778            ),
13779            PktLineFrame::Data(
13780                b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec(),
13781            ),
13782            PktLineFrame::Data(
13783                b"2222222222222222222222222222222222222222 refs/tags/v1 peeled:3333333333333333333333333333333333333333\n"
13784                    .to_vec(),
13785            ),
13786            PktLineFrame::Flush,
13787        ];
13788
13789        let set = parse_protocol_v2_ls_refs_response_as_ref_advertisement_set(
13790            ObjectFormat::Sha1,
13791            &frames,
13792        )
13793        .expect("test operation should succeed");
13794        assert_eq!(
13795            set,
13796            RefAdvertisementSet {
13797                protocol: ProtocolVersion::V2,
13798                refs: vec![
13799                    RefAdvertisement {
13800                        oid: head.clone(),
13801                        name: "HEAD".into(),
13802                        capabilities: vec![Capability {
13803                            name: "symref".into(),
13804                            value: Some("HEAD:refs/heads/main".into()),
13805                        }],
13806                    },
13807                    RefAdvertisement {
13808                        oid: head,
13809                        name: "refs/heads/main".into(),
13810                        capabilities: Vec::new(),
13811                    },
13812                    RefAdvertisement {
13813                        oid: tag,
13814                        name: "refs/tags/v1".into(),
13815                        capabilities: Vec::new(),
13816                    },
13817                    RefAdvertisement {
13818                        oid: tag_peeled,
13819                        name: "refs/tags/v1^{}".into(),
13820                        capabilities: Vec::new(),
13821                    },
13822                ],
13823                shallow: Vec::new(),
13824            }
13825        );
13826
13827        // The streaming reader path produces the same bridged set.
13828        let mut encoded = Vec::new();
13829        write_pkt_line_frames(&mut encoded, &frames).expect("test operation should succeed");
13830        encoded.extend_from_slice(b"tail");
13831        let mut input = encoded.as_slice();
13832        assert_eq!(
13833            read_protocol_v2_ls_refs_response_as_ref_advertisement_set(
13834                ObjectFormat::Sha1,
13835                &mut input,
13836            )
13837            .expect("test operation should succeed"),
13838            set,
13839        );
13840        assert_eq!(input, b"tail");
13841    }
13842
13843    #[test]
13844    fn protocol_v2_ls_refs_records_bridge_unborn_head_symref_and_empty() {
13845        // An unborn HEAD pointing at an as-yet-uncreated branch carries only a
13846        // symref capability and has no concrete ref to attach it to.
13847        let records = vec![ProtocolV2LsRefsRecord::Unborn {
13848            name: "HEAD".into(),
13849            symref_target: Some("refs/heads/main".into()),
13850            attributes: Vec::new(),
13851        }];
13852        assert!(protocol_v2_ls_refs_records_to_ref_advertisement_set(&records).is_err());
13853
13854        // An empty ls-refs response bridges to an empty v2 set.
13855        assert_eq!(
13856            protocol_v2_ls_refs_records_to_ref_advertisement_set(&[])
13857                .expect("test operation should succeed"),
13858            RefAdvertisementSet {
13859                protocol: ProtocolVersion::V2,
13860                refs: Vec::new(),
13861                shallow: Vec::new(),
13862            }
13863        );
13864
13865        // An unborn HEAD alongside a concrete ref attaches the symref to the
13866        // first ref, matching the v0/v1 advertisement convention.
13867        let main = ObjectId::from_hex(
13868            ObjectFormat::Sha1,
13869            "4444444444444444444444444444444444444444",
13870        )
13871        .expect("test operation should succeed");
13872        let records = vec![
13873            ProtocolV2LsRefsRecord::Unborn {
13874                name: "HEAD".into(),
13875                symref_target: Some("refs/heads/main".into()),
13876                attributes: Vec::new(),
13877            },
13878            ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
13879                oid: main.clone(),
13880                name: "refs/heads/main".into(),
13881                peeled: None,
13882                symref_target: None,
13883                attributes: Vec::new(),
13884            }),
13885        ];
13886        let set = protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
13887            .expect("test operation should succeed");
13888        assert_eq!(
13889            set,
13890            RefAdvertisementSet {
13891                protocol: ProtocolVersion::V2,
13892                refs: vec![RefAdvertisement {
13893                    oid: main,
13894                    name: "refs/heads/main".into(),
13895                    capabilities: vec![Capability {
13896                        name: "symref".into(),
13897                        value: Some("HEAD:refs/heads/main".into()),
13898                    }],
13899                }],
13900                shallow: Vec::new(),
13901            }
13902        );
13903    }
13904}