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!(
97        "packet: {:>12}{} ",
98        packet_trace_prefix(),
99        if is_write { '>' } else { '<' }
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
116pub fn trace_packet_read_payload(payload: &[u8]) {
117    packet_trace(payload, false);
118}
119
120pub fn trace_packet_write_payload(payload: &[u8]) {
121    packet_trace(payload, true);
122}
123
124/// Trace a frame on the wire. Flush/delim/response-end map to their 4-byte
125/// tokens (`0000`/`0001`/`0002`) like git, data frames to their payload.
126fn packet_trace_frame(frame: &PktLineFrame, is_write: bool) {
127    if !packet_trace_enabled() {
128        return;
129    }
130    match frame {
131        PktLineFrame::Data(payload) => packet_trace(payload, is_write),
132        PktLineFrame::Flush => packet_trace(b"0000", is_write),
133        PktLineFrame::Delimiter => packet_trace(b"0001", is_write),
134        PktLineFrame::ResponseEnd => packet_trace(b"0002", is_write),
135    }
136}
137
138pub const PKT_LINE_MAX_LEN: usize = 65_520;
139
140pub const PKT_LINE_MAX_PAYLOAD_LEN: usize = PKT_LINE_MAX_LEN - 4;
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum ProtocolVersion {
144    V0,
145    V1,
146    V2,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct PktLine(pub Vec<u8>);
151
152impl PktLine {
153    pub fn encode(&self) -> Vec<u8> {
154        encode_pkt_line_payload(&self.0)
155    }
156
157    pub fn try_encode(&self) -> Result<Vec<u8>> {
158        validate_pkt_line_payload(&self.0)?;
159        Ok(self.encode())
160    }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub enum PktLineFrame {
165    Data(Vec<u8>),
166    Flush,
167    Delimiter,
168    ResponseEnd,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct ProtocolErrorLine {
173    pub message: String,
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum GitService {
178    UploadPack,
179    ReceivePack,
180    UploadArchive,
181}
182
183impl GitService {
184    pub fn as_str(self) -> &'static str {
185        match self {
186            Self::UploadPack => "git-upload-pack",
187            Self::ReceivePack => "git-receive-pack",
188            Self::UploadArchive => "git-upload-archive",
189        }
190    }
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct RefSpec {
195    pub force: bool,
196    pub negative: bool,
197    pub src: Option<String>,
198    pub dst: Option<String>,
199    pub pattern: bool,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct FetchHeadRecord {
204    pub oid: ObjectId,
205    pub not_for_merge: bool,
206    pub description: String,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct FetchRefUpdate {
211    pub src: String,
212    pub dst: Option<String>,
213    pub oid: ObjectId,
214    pub not_for_merge: bool,
215    pub force: bool,
216}
217
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct PushSourceRef {
220    pub name: String,
221    pub oid: ObjectId,
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225pub enum SideBandChannel {
226    Data,
227    Progress,
228    Fatal,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
232pub struct SideBandPacket {
233    pub channel: SideBandChannel,
234    pub data: Vec<u8>,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Default)]
238pub struct SideBandDemux {
239    pub data: Vec<u8>,
240    pub progress: Vec<Vec<u8>>,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Default)]
244pub struct UploadArchiveRequest {
245    pub arguments: Vec<String>,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq)]
249pub enum UploadArchiveResponse {
250    Ack { sideband: Vec<SideBandPacket> },
251    Nack { message: String },
252}
253
254impl PktLineFrame {
255    pub fn data(payload: impl Into<Vec<u8>>) -> Result<Self> {
256        let payload = payload.into();
257        validate_pkt_line_payload(&payload)?;
258        Ok(Self::Data(payload))
259    }
260
261    pub fn encode(&self) -> Vec<u8> {
262        match self {
263            Self::Data(payload) => encode_pkt_line_payload(payload),
264            Self::Flush => b"0000".to_vec(),
265            Self::Delimiter => b"0001".to_vec(),
266            Self::ResponseEnd => b"0002".to_vec(),
267        }
268    }
269
270    pub fn try_encode(&self) -> Result<Vec<u8>> {
271        match self {
272            Self::Data(payload) => try_encode_pkt_line_payload(payload),
273            Self::Flush | Self::Delimiter | Self::ResponseEnd => Ok(self.encode()),
274        }
275    }
276
277    pub fn parse(input: &[u8]) -> Result<(Self, usize)> {
278        if input.len() < 4 {
279            return Err(GitError::InvalidFormat("truncated pkt-line length".into()));
280        }
281        let len = parse_pkt_len(&input[..4])?;
282        match len {
283            0 => Ok((Self::Flush, 4)),
284            1 => Ok((Self::Delimiter, 4)),
285            2 => Ok((Self::ResponseEnd, 4)),
286            3 => Err(GitError::InvalidFormat(
287                "reserved pkt-line length 0003".into(),
288            )),
289            4..=PKT_LINE_MAX_LEN => {
290                if input.len() < len {
291                    return Err(GitError::InvalidFormat(format!(
292                        "truncated pkt-line payload: expected {} bytes, got {}",
293                        len - 4,
294                        input.len().saturating_sub(4)
295                    )));
296                }
297                Ok((Self::Data(input[4..len].to_vec()), len))
298            }
299            _ => Err(GitError::InvalidFormat(format!(
300                "pkt-line length exceeds {PKT_LINE_MAX_LEN}: {len}"
301            ))),
302        }
303    }
304}
305
306fn validate_pkt_line_payload(payload: &[u8]) -> Result<()> {
307    if payload.len() > PKT_LINE_MAX_PAYLOAD_LEN {
308        return Err(GitError::InvalidFormat(format!(
309            "pkt-line payload exceeds {PKT_LINE_MAX_PAYLOAD_LEN} bytes"
310        )));
311    }
312    Ok(())
313}
314
315fn pkt_line_header(len: usize) -> [u8; 4] {
316    const HEX: &[u8; 16] = b"0123456789abcdef";
317    [
318        HEX[(len >> 12) & 0xf],
319        HEX[(len >> 8) & 0xf],
320        HEX[(len >> 4) & 0xf],
321        HEX[len & 0xf],
322    ]
323}
324
325fn encode_pkt_line_payload(payload: &[u8]) -> Vec<u8> {
326    let len = payload.len() + 4;
327    let mut out = Vec::with_capacity(len);
328    out.extend_from_slice(&pkt_line_header(len));
329    out.extend_from_slice(payload);
330    out
331}
332
333fn try_encode_pkt_line_payload(payload: &[u8]) -> Result<Vec<u8>> {
334    validate_pkt_line_payload(payload)?;
335    Ok(encode_pkt_line_payload(payload))
336}
337
338pub fn parse_pkt_line_stream(mut input: &[u8]) -> Result<Vec<PktLineFrame>> {
339    let mut frames = Vec::new();
340    while !input.is_empty() {
341        let (frame, consumed) = PktLineFrame::parse(input)?;
342        frames.push(frame);
343        input = &input[consumed..];
344    }
345    Ok(frames)
346}
347
348fn parse_pkt_line_frames_until_flush_from(mut input: &[u8]) -> Result<(Vec<PktLineFrame>, usize)> {
349    let mut frames = Vec::new();
350    let mut total = 0usize;
351    loop {
352        if input.is_empty() {
353            return Err(GitError::InvalidFormat(
354                "pkt-line stream ended before flush".into(),
355            ));
356        }
357        let (frame, consumed) = PktLineFrame::parse(input)?;
358        total += consumed;
359        let done = matches!(frame, PktLineFrame::Flush);
360        frames.push(frame);
361        input = &input[consumed..];
362        if done {
363            return Ok((frames, total));
364        }
365    }
366}
367
368pub fn read_pkt_line_frame(reader: &mut impl Read) -> Result<Option<PktLineFrame>> {
369    let mut header = [0u8; 4];
370    let mut read = 0usize;
371    while read < header.len() {
372        match reader.read(&mut header[read..]) {
373            Ok(0) if read == 0 => return Ok(None),
374            Ok(0) => {
375                return Err(GitError::InvalidFormat("truncated pkt-line length".into()));
376            }
377            Ok(n) => read += n,
378            Err(err) if err.kind() == ErrorKind::Interrupted => {}
379            Err(err) => return Err(err.into()),
380        }
381    }
382
383    let len = parse_pkt_len(&header)?;
384    let frame = match len {
385        0 => PktLineFrame::Flush,
386        1 => PktLineFrame::Delimiter,
387        2 => PktLineFrame::ResponseEnd,
388        3 => {
389            return Err(GitError::InvalidFormat(
390                "reserved pkt-line length 0003".into(),
391            ));
392        }
393        4..=PKT_LINE_MAX_LEN => {
394            let mut payload = vec![0; len - 4];
395            reader.read_exact(&mut payload)?;
396            PktLineFrame::Data(payload)
397        }
398        _ => {
399            return Err(GitError::InvalidFormat(format!(
400                "pkt-line length exceeds {PKT_LINE_MAX_LEN}: {len}"
401            )));
402        }
403    };
404    packet_trace_frame(&frame, false);
405    Ok(Some(frame))
406}
407
408pub fn read_pkt_line_frames(reader: &mut impl Read) -> Result<Vec<PktLineFrame>> {
409    let mut frames = Vec::new();
410    while let Some(frame) = read_pkt_line_frame(reader)? {
411        frames.push(frame);
412    }
413    Ok(frames)
414}
415
416pub fn read_pkt_line_frames_until_flush(reader: &mut impl Read) -> Result<Vec<PktLineFrame>> {
417    read_pkt_line_frames_until_control(reader, |frame| matches!(frame, PktLineFrame::Flush))
418}
419
420pub fn read_pkt_line_frames_until_response_end(
421    reader: &mut impl Read,
422) -> Result<Vec<PktLineFrame>> {
423    read_pkt_line_frames_until_control(reader, |frame| matches!(frame, PktLineFrame::ResponseEnd))
424}
425
426fn read_pkt_line_frames_until_control(
427    reader: &mut impl Read,
428    stop: impl Fn(&PktLineFrame) -> bool,
429) -> Result<Vec<PktLineFrame>> {
430    let mut frames = Vec::new();
431    loop {
432        let Some(frame) = read_pkt_line_frame(reader)? else {
433            return Err(GitError::InvalidFormat(
434                "pkt-line stream ended before control packet".into(),
435            ));
436        };
437        let done = stop(&frame);
438        frames.push(frame);
439        if done {
440            return Ok(frames);
441        }
442    }
443}
444
445pub fn write_pkt_line_frame(writer: &mut impl Write, frame: &PktLineFrame) -> Result<()> {
446    match frame {
447        // `write_pkt_line_payload` already traces the data line.
448        PktLineFrame::Data(payload) => write_pkt_line_payload(writer, payload)?,
449        PktLineFrame::Flush => {
450            packet_trace(b"0000", true);
451            writer.write_all(b"0000")?;
452        }
453        PktLineFrame::Delimiter => {
454            packet_trace(b"0001", true);
455            writer.write_all(b"0001")?;
456        }
457        PktLineFrame::ResponseEnd => {
458            packet_trace(b"0002", true);
459            writer.write_all(b"0002")?;
460        }
461    }
462    Ok(())
463}
464
465pub fn write_pkt_line_payload(writer: &mut impl Write, payload: &[u8]) -> Result<()> {
466    validate_pkt_line_payload(payload)?;
467    packet_trace(payload, true);
468    let len = payload.len() + 4;
469    writer.write_all(&pkt_line_header(len))?;
470    writer.write_all(payload)?;
471    Ok(())
472}
473
474pub fn write_pkt_line_frames(writer: &mut impl Write, frames: &[PktLineFrame]) -> Result<()> {
475    for frame in frames {
476        write_pkt_line_frame(writer, frame)?;
477    }
478    Ok(())
479}
480
481pub fn parse_error_line(payload: &[u8]) -> Result<ProtocolErrorLine> {
482    let text = parse_protocol_v2_line_text("protocol error line", payload)?;
483    let Some(message) = text.strip_prefix("ERR ") else {
484        return Err(GitError::InvalidFormat(
485            "protocol error line must start with ERR".into(),
486        ));
487    };
488    validate_protocol_error_message(message)?;
489    Ok(ProtocolErrorLine {
490        message: message.to_string(),
491    })
492}
493
494pub fn encode_error_line(error: &ProtocolErrorLine) -> Result<Vec<u8>> {
495    validate_protocol_error_message(&error.message)?;
496    Ok(line_from_str(&format!("ERR {}", error.message)))
497}
498
499pub fn parse_error_frame(frame: &PktLineFrame) -> Result<Option<ProtocolErrorLine>> {
500    match frame {
501        PktLineFrame::Data(payload) if trim_trailing_lf(payload).starts_with(b"ERR ") => {
502            parse_error_line(payload).map(Some)
503        }
504        PktLineFrame::Data(_)
505        | PktLineFrame::Flush
506        | PktLineFrame::Delimiter
507        | PktLineFrame::ResponseEnd => Ok(None),
508    }
509}
510
511pub fn read_error_line(reader: &mut impl Read) -> Result<ProtocolErrorLine> {
512    let Some(frame) = read_pkt_line_frame(reader)? else {
513        return Err(GitError::InvalidFormat(
514            "pkt-line stream ended before protocol error line".into(),
515        ));
516    };
517    match frame {
518        PktLineFrame::Data(payload) => parse_error_line(&payload),
519        _ => Err(GitError::InvalidFormat(
520            "protocol error line must be a data packet".into(),
521        )),
522    }
523}
524
525pub fn write_error_line(writer: &mut impl Write, error: &ProtocolErrorLine) -> Result<()> {
526    write_pkt_line_frame(writer, &PktLineFrame::data(encode_error_line(error)?)?)
527}
528
529pub fn parse_git_service(value: &str) -> Result<GitService> {
530    match value {
531        "git-upload-pack" => Ok(GitService::UploadPack),
532        "git-receive-pack" => Ok(GitService::ReceivePack),
533        "git-upload-archive" => Ok(GitService::UploadArchive),
534        other => Err(GitError::InvalidFormat(format!(
535            "unsupported git service {other}"
536        ))),
537    }
538}
539
540pub fn parse_refspec(value: &str) -> Result<RefSpec> {
541    validate_refspec_value(value)?;
542    let (force, value) = value
543        .strip_prefix('+')
544        .map_or((false, value), |value| (true, value));
545    let (negative, value) = value
546        .strip_prefix('^')
547        .map_or((false, value), |value| (true, value));
548    if force && negative {
549        return Err(GitError::InvalidFormat(
550            "negative refspec must not be forced".into(),
551        ));
552    }
553    let (src, dst) = if negative {
554        if value.contains(':') {
555            return Err(GitError::InvalidFormat(
556                "negative refspec must not have a destination".into(),
557            ));
558        }
559        (Some(value), None)
560    } else if let Some((src, dst)) = value.split_once(':') {
561        (non_empty(src), non_empty(dst))
562    } else {
563        (Some(value), None)
564    };
565    if src.is_none() && dst.is_none() && value != ":" {
566        return Err(GitError::InvalidFormat(
567            "refspec must include a source or destination".into(),
568        ));
569    }
570    if negative && src.is_none() {
571        return Err(GitError::InvalidFormat(
572            "negative refspec is missing a source".into(),
573        ));
574    }
575    if let Some(src) = src {
576        validate_refspec_endpoint("refspec source", src)?;
577    }
578    if let Some(dst) = dst {
579        validate_refspec_endpoint("refspec destination", dst)?;
580    }
581    let src_pattern_count = src.map(count_refspec_wildcards).unwrap_or(0);
582    let dst_pattern_count = dst.map(count_refspec_wildcards).unwrap_or(0);
583    if src_pattern_count > 1 || dst_pattern_count > 1 {
584        return Err(GitError::InvalidFormat(
585            "refspec endpoint has too many wildcards".into(),
586        ));
587    }
588    if dst.is_some() && (src_pattern_count == 1) != (dst_pattern_count == 1) {
589        return Err(GitError::InvalidFormat(
590            "refspec wildcard must appear in both source and destination".into(),
591        ));
592    }
593    Ok(RefSpec {
594        force,
595        negative,
596        src: src.map(str::to_string),
597        dst: dst.map(str::to_string),
598        pattern: src_pattern_count == 1 || dst_pattern_count == 1,
599    })
600}
601
602pub fn encode_refspec(refspec: &RefSpec) -> Result<String> {
603    validate_refspec_shape(refspec)?;
604    let mut out = String::new();
605    if refspec.force {
606        out.push('+');
607    }
608    if refspec.negative {
609        out.push('^');
610    }
611    if let Some(src) = &refspec.src {
612        out.push_str(src);
613    }
614    if !refspec.negative && refspec.src.is_none() && refspec.dst.is_none() {
615        out.push(':');
616    } else if !refspec.negative && refspec.dst.is_some() {
617        out.push(':');
618        if let Some(dst) = &refspec.dst {
619            out.push_str(dst);
620        }
621    }
622    Ok(out)
623}
624
625pub fn refspec_matches_source(refspec: &RefSpec, source: &str) -> Result<bool> {
626    Ok(refspec_map_source(refspec, source)?.is_some())
627}
628
629pub fn refspec_map_source(refspec: &RefSpec, source: &str) -> Result<Option<String>> {
630    validate_refspec_shape(refspec)?;
631    validate_refspec_endpoint("refspec match source", source)?;
632    let Some(src) = refspec.src.as_deref() else {
633        return Ok(None);
634    };
635    if refspec.pattern {
636        let Some((src_prefix, src_suffix)) = src.split_once('*') else {
637            return Ok(None);
638        };
639        let Some(middle) = source
640            .strip_prefix(src_prefix)
641            .and_then(|value| value.strip_suffix(src_suffix))
642        else {
643            return Ok(None);
644        };
645        if let Some(dst) = refspec.dst.as_deref() {
646            let (dst_prefix, dst_suffix) = dst.split_once('*').ok_or_else(|| {
647                GitError::InvalidFormat("pattern refspec destination is missing wildcard".into())
648            })?;
649            return Ok(Some(format!("{dst_prefix}{middle}{dst_suffix}")));
650        }
651        return Ok(Some(source.to_string()));
652    }
653    if src == source {
654        return Ok(Some(
655            refspec.dst.clone().unwrap_or_else(|| source.to_string()),
656        ));
657    }
658    Ok(None)
659}
660
661pub fn fetch_head_ref_description(refname: &str) -> Result<String> {
662    validate_fetch_head_description_field(refname)?;
663    // Mirror git's `kind`/`what` split in builtin/fetch.c: `HEAD` yields an empty
664    // note (no `'…' of` prefix at all), the standard ref namespaces get their
665    // kind word, and any other ref name is quoted bare.
666    if refname == "HEAD" {
667        Ok(String::new())
668    } else if let Some(branch) = refname.strip_prefix("refs/heads/") {
669        Ok(format!("branch '{branch}'"))
670    } else if let Some(tag) = refname.strip_prefix("refs/tags/") {
671        Ok(format!("tag '{tag}'"))
672    } else if let Some(rest) = refname.strip_prefix("refs/remotes/") {
673        Ok(format!("remote-tracking branch '{rest}'"))
674    } else {
675        Ok(format!("'{refname}'"))
676    }
677}
678
679pub fn fetch_head_remote_description(refname: &str, remote: &str) -> Result<String> {
680    validate_fetch_head_description_field(remote)?;
681    // git only appends `of <url>` when the note (`what`) is non-empty; a bare
682    // `HEAD` fetch records just the URL with an empty description.
683    let what = fetch_head_ref_description(refname)?;
684    if what.is_empty() {
685        Ok(remote.to_string())
686    } else {
687        Ok(format!("{what} of {remote}"))
688    }
689}
690
691pub fn parse_fetch_head(format: ObjectFormat, input: &[u8]) -> Result<Vec<FetchHeadRecord>> {
692    if input.is_empty() {
693        return Ok(Vec::new());
694    }
695    input
696        .split_inclusive(|byte| *byte == b'\n')
697        .map(|line| parse_fetch_head_record(format, line))
698        .collect()
699}
700
701pub fn encode_fetch_head(records: &[FetchHeadRecord]) -> Result<Vec<u8>> {
702    let mut out = Vec::new();
703    for record in records {
704        validate_fetch_head_description_field(&record.description)?;
705        out.extend_from_slice(record.oid.to_string().as_bytes());
706        out.push(b'\t');
707        if record.not_for_merge {
708            out.extend_from_slice(b"not-for-merge");
709        }
710        out.push(b'\t');
711        out.extend_from_slice(record.description.as_bytes());
712        out.push(b'\n');
713    }
714    Ok(out)
715}
716
717pub fn read_fetch_head(
718    format: ObjectFormat,
719    reader: &mut impl Read,
720) -> Result<Vec<FetchHeadRecord>> {
721    let mut input = Vec::new();
722    reader.read_to_end(&mut input)?;
723    parse_fetch_head(format, &input)
724}
725
726pub fn write_fetch_head(writer: &mut impl Write, records: &[FetchHeadRecord]) -> Result<()> {
727    for record in records {
728        validate_fetch_head_description_field(&record.description)?;
729        writer.write_all(record.oid.to_string().as_bytes())?;
730        writer.write_all(b"\t")?;
731        if record.not_for_merge {
732            writer.write_all(b"not-for-merge")?;
733        }
734        writer.write_all(b"\t")?;
735        writer.write_all(record.description.as_bytes())?;
736        writer.write_all(b"\n")?;
737    }
738    Ok(())
739}
740
741/// Match an abbreviated refspec source against the advertised refs the way
742/// upstream's `find_ref_by_name_abbrev` (remote.c) does: score each
743/// advertisement with `refname_match`'s `ref_rev_parse_rules` (exact name
744/// first, then `refs/<name>`, `refs/tags/<name>`, `refs/heads/<name>`,
745/// `refs/remotes/<name>`, `refs/remotes/<name>/HEAD`) and keep the best.
746fn find_advertised_ref_by_name_abbrev<'a>(
747    refs: &'a [RefAdvertisement],
748    name: &str,
749) -> Option<&'a RefAdvertisement> {
750    let mut best: Option<(&RefAdvertisement, usize)> = None;
751    for reference in refs {
752        let score = fetch_refname_match_score(name, &reference.name);
753        if score > best.map(|(_, score)| score).unwrap_or(0) {
754            best = Some((reference, score));
755        }
756    }
757    best.map(|(reference, _)| reference)
758}
759
760/// `refname_match` (refs.c): non-zero when `abbrev` can mean `full`, with the
761/// magnitude giving disambiguation precedence (earlier rules win).
762fn fetch_refname_match_score(abbrev: &str, full: &str) -> usize {
763    let expansions = [
764        abbrev.to_string(),
765        format!("refs/{abbrev}"),
766        format!("refs/tags/{abbrev}"),
767        format!("refs/heads/{abbrev}"),
768        format!("refs/remotes/{abbrev}"),
769        format!("refs/remotes/{abbrev}/HEAD"),
770    ];
771    for (index, candidate) in expansions.iter().enumerate() {
772        if candidate == full {
773            return expansions.len() - index;
774        }
775    }
776    0
777}
778
779/// Whether `abbrev` (a possibly-abbreviated ref like `three` or `refs/heads/main`)
780/// matches the full ref `full` under git's `ref_rev_parse_rules` expansion, the
781/// way `refname_match`/`branch_merge_matches` (remote.c) compare a configured
782/// `branch.<name>.merge` value against an advertised ref name.
783pub fn refname_matches(abbrev: &str, full: &str) -> bool {
784    fetch_refname_match_score(abbrev, full) > 0
785}
786
787/// Qualify a fetch refspec destination the way upstream's `get_local_ref`
788/// (remote.c) does: `refs/...` stays as-is, `heads/`, `tags/` and `remotes/`
789/// gain a `refs/` prefix, and anything else lands under `refs/heads/`.
790fn fetch_local_ref_name(name: &str) -> String {
791    if name.starts_with("refs/") {
792        name.to_string()
793    } else if name.starts_with("heads/")
794        || name.starts_with("tags/")
795        || name.starts_with("remotes/")
796    {
797        format!("refs/{name}")
798    } else {
799        format!("refs/heads/{name}")
800    }
801}
802
803pub fn plan_fetch_ref_updates(
804    refs: &[RefAdvertisement],
805    refspecs: &[RefSpec],
806    auto_follow_tags: bool,
807) -> Result<Vec<FetchRefUpdate>> {
808    let negative = refspecs
809        .iter()
810        .filter(|refspec| refspec.negative)
811        .collect::<Vec<_>>();
812    let mut updates = Vec::new();
813    for refspec in refspecs.iter().filter(|refspec| !refspec.negative) {
814        validate_refspec_shape(refspec)?;
815        let Some(src) = refspec.src.as_deref() else {
816            return Err(GitError::InvalidFormat(
817                "fetch refspec is missing a source".into(),
818            ));
819        };
820        if refspec.pattern {
821            for reference in refs {
822                if refspec_is_excluded(&negative, &reference.name)? {
823                    continue;
824                }
825                if let Some(dst) = refspec_map_source(refspec, &reference.name)? {
826                    updates.push(FetchRefUpdate {
827                        src: reference.name.clone(),
828                        dst: Some(dst),
829                        oid: reference.oid,
830                        not_for_merge: false,
831                        force: refspec.force,
832                    });
833                }
834            }
835            continue;
836        }
837        if refspec_is_excluded(&negative, src)? {
838            continue;
839        }
840        let Some(reference) = find_advertised_ref_by_name_abbrev(refs, src) else {
841            return Err(GitError::reference_not_found(format!("remote ref {src}")));
842        };
843        updates.push(FetchRefUpdate {
844            src: reference.name.clone(),
845            dst: refspec.dst.as_deref().map(fetch_local_ref_name),
846            oid: reference.oid,
847            not_for_merge: false,
848            force: refspec.force,
849        });
850    }
851    if auto_follow_tags && updates.iter().any(|update| update.dst.is_some()) {
852        let fetched_oids = updates.iter().map(|update| update.oid).collect::<Vec<_>>();
853        let fetched_srcs = updates
854            .iter()
855            .map(|update| update.src.clone())
856            .collect::<Vec<_>>();
857        for reference in refs {
858            if reference.name.starts_with("refs/tags/")
859                && fetched_oids.iter().any(|oid| oid == &reference.oid)
860                && !fetched_srcs.contains(&reference.name)
861                && !refspec_is_excluded(&negative, &reference.name)?
862            {
863                updates.push(FetchRefUpdate {
864                    src: reference.name.clone(),
865                    dst: Some(reference.name.clone()),
866                    oid: reference.oid,
867                    not_for_merge: true,
868                    force: false,
869                });
870            }
871        }
872    }
873    Ok(updates)
874}
875
876pub fn fetch_ref_updates_to_fetch_head(
877    updates: &[FetchRefUpdate],
878    remote: &str,
879) -> Result<Vec<FetchHeadRecord>> {
880    updates
881        .iter()
882        .map(|update| {
883            Ok(FetchHeadRecord {
884                oid: update.oid,
885                not_for_merge: update.not_for_merge,
886                description: fetch_head_remote_description(&update.src, remote)?,
887            })
888        })
889        .collect()
890}
891
892pub fn plan_push_commands(
893    format: ObjectFormat,
894    local_refs: &[PushSourceRef],
895    remote_refs: &[RefAdvertisement],
896    refspecs: &[RefSpec],
897) -> Result<Vec<ReceivePackCommand>> {
898    let zero = zero_object_id(format)?;
899    let mut commands = Vec::new();
900    for refspec in refspecs {
901        validate_refspec_shape(refspec)?;
902        if refspec.negative {
903            return Err(GitError::InvalidFormat(
904                "push refspec must not be negative".into(),
905            ));
906        }
907        match (refspec.src.as_deref(), refspec.dst.as_deref()) {
908            (None, None) => {
909                // A bare ":" (matching) refspec pushes only refs the remote
910                // already has, by their fully-qualified `refs/...` name. git's
911                // matching source set is the local ref advertisement, which never
912                // includes `HEAD` or short-name aliases — push those would try to
913                // update the remote's `HEAD`, so skip anything not under `refs/`.
914                for local in local_refs {
915                    if !local.name.starts_with("refs/") {
916                        continue;
917                    }
918                    validate_push_source_ref(format, local)?;
919                    if let Some(remote) = remote_ref(remote_refs, &local.name) {
920                        commands.push(ReceivePackCommand {
921                            old_id: remote.oid,
922                            new_id: local.oid,
923                            name: local.name.clone(),
924                        });
925                    }
926                }
927            }
928            (None, Some(dst)) => {
929                validate_refspec_endpoint("push destination", dst)?;
930                let old_id = remote_ref(remote_refs, dst)
931                    .map(|reference| reference.oid)
932                    .unwrap_or_else(|| zero.clone());
933                commands.push(ReceivePackCommand {
934                    old_id,
935                    new_id: zero.clone(),
936                    name: dst.to_string(),
937                });
938            }
939            (Some(src), dst) if refspec.pattern => {
940                let Some((src_prefix, src_suffix)) = src.split_once('*') else {
941                    return Err(GitError::InvalidFormat(
942                        "pattern push refspec source is missing wildcard".into(),
943                    ));
944                };
945                let dst = dst.ok_or_else(|| {
946                    GitError::InvalidFormat("pattern push refspec is missing destination".into())
947                })?;
948                let (dst_prefix, dst_suffix) = dst.split_once('*').ok_or_else(|| {
949                    GitError::InvalidFormat(
950                        "pattern push refspec destination is missing wildcard".into(),
951                    )
952                })?;
953                for local in local_refs {
954                    validate_push_source_ref(format, local)?;
955                    let Some(middle) = local
956                        .name
957                        .strip_prefix(src_prefix)
958                        .and_then(|value| value.strip_suffix(src_suffix))
959                    else {
960                        continue;
961                    };
962                    let name = format!("{dst_prefix}{middle}{dst_suffix}");
963                    let old_id = remote_ref(remote_refs, &name)
964                        .map(|reference| reference.oid)
965                        .unwrap_or_else(|| zero.clone());
966                    commands.push(ReceivePackCommand {
967                        old_id,
968                        new_id: local.oid,
969                        name,
970                    });
971                }
972            }
973            (Some(src), dst) => {
974                validate_refspec_endpoint("push source", src)?;
975                let local = local_ref(local_refs, src)
976                    .ok_or_else(|| GitError::reference_not_found(format!("local ref {src}")))?;
977                validate_push_source_ref(format, local)?;
978                let name = dst.unwrap_or(src);
979                validate_refspec_endpoint("push destination", name)?;
980                let old_id = remote_ref(remote_refs, name)
981                    .map(|reference| reference.oid)
982                    .unwrap_or_else(|| zero.clone());
983                commands.push(ReceivePackCommand {
984                    old_id,
985                    new_id: local.oid,
986                    name: name.to_string(),
987                });
988            }
989        }
990    }
991    Ok(commands)
992}
993
994pub fn build_receive_pack_push_request(
995    features: &ReceivePackFeatures,
996    commands: Vec<ReceivePackCommand>,
997    packfile: Vec<u8>,
998    options: ReceivePackPushRequestOptions,
999) -> Result<ReceivePackPushRequest> {
1000    let header = build_receive_pack_push_request_header(features, commands, options)?;
1001    let request = ReceivePackPushRequest {
1002        commands: header.commands,
1003        push_options: header.push_options,
1004        packfile,
1005    };
1006    validate_receive_pack_push_request_features(features, &request)?;
1007    Ok(request)
1008}
1009
1010pub fn build_receive_pack_push_request_header(
1011    features: &ReceivePackFeatures,
1012    commands: Vec<ReceivePackCommand>,
1013    options: ReceivePackPushRequestOptions,
1014) -> Result<ReceivePackPushRequestHeader> {
1015    let mut capabilities = Vec::new();
1016    if options.report_status_v2 {
1017        require_receive_pack_feature(features.report_status_v2, "report-status-v2")?;
1018        capabilities.push(Capability {
1019            name: "report-status-v2".into(),
1020            value: None,
1021        });
1022    } else if options.report_status {
1023        require_receive_pack_feature(features.report_status, "report-status")?;
1024        capabilities.push(Capability {
1025            name: "report-status".into(),
1026            value: None,
1027        });
1028    }
1029    if commands.iter().any(is_receive_pack_delete_command) {
1030        require_receive_pack_feature(features.delete_refs, "delete-refs")?;
1031        capabilities.push(Capability {
1032            name: "delete-refs".into(),
1033            value: None,
1034        });
1035    }
1036    if options.atomic {
1037        require_receive_pack_feature(features.atomic, "atomic")?;
1038        capabilities.push(Capability {
1039            name: "atomic".into(),
1040            value: None,
1041        });
1042    }
1043    if options.ofs_delta {
1044        require_receive_pack_feature(features.ofs_delta, "ofs-delta")?;
1045        capabilities.push(Capability {
1046            name: "ofs-delta".into(),
1047            value: None,
1048        });
1049    }
1050    if options.side_band_64k {
1051        require_receive_pack_feature(features.side_band_64k, "side-band-64k")?;
1052        capabilities.push(Capability {
1053            name: "side-band-64k".into(),
1054            value: None,
1055        });
1056    }
1057    if options.quiet {
1058        require_receive_pack_feature(features.quiet, "quiet")?;
1059        capabilities.push(Capability {
1060            name: "quiet".into(),
1061            value: None,
1062        });
1063    }
1064    if let Some(agent) = &options.agent {
1065        validate_capability_field("receive-pack request agent", agent)?;
1066        capabilities.push(Capability {
1067            name: "agent".into(),
1068            value: Some(agent.clone()),
1069        });
1070    }
1071    if let Some(format) = options.object_format {
1072        if features.object_format != Some(format) {
1073            return Err(GitError::InvalidFormat(
1074                "receive-pack request object-format was not advertised".into(),
1075            ));
1076        }
1077        capabilities.push(Capability {
1078            name: "object-format".into(),
1079            value: Some(format.name().into()),
1080        });
1081    }
1082    let push_options = if options.push_options.is_empty() {
1083        None
1084    } else {
1085        require_receive_pack_feature(features.push_options, "push-options")?;
1086        for option in &options.push_options {
1087            validate_receive_pack_push_option(option.as_bytes())?;
1088        }
1089        capabilities.push(Capability {
1090            name: "push-options".into(),
1091            value: None,
1092        });
1093        Some(options.push_options)
1094    };
1095    let header = ReceivePackPushRequestHeader {
1096        commands: ReceivePackRequest {
1097            commands,
1098            capabilities,
1099            shallow: Vec::new(),
1100        },
1101        push_options,
1102    };
1103    validate_receive_pack_push_request_features(
1104        features,
1105        &ReceivePackPushRequest {
1106            commands: header.commands.clone(),
1107            push_options: header.push_options.clone(),
1108            packfile: Vec::new(),
1109        },
1110    )?;
1111    Ok(header)
1112}
1113
1114pub fn smart_http_info_refs_path(repository_path: &str, service: GitService) -> Result<String> {
1115    validate_smart_http_service(service)?;
1116    let repository_path = normalize_http_repository_path(repository_path)?;
1117    Ok(format!(
1118        "{repository_path}/info/refs?service={}",
1119        service.as_str()
1120    ))
1121}
1122
1123pub fn smart_http_rpc_path(repository_path: &str, service: GitService) -> Result<String> {
1124    validate_smart_http_service(service)?;
1125    let repository_path = normalize_http_repository_path(repository_path)?;
1126    Ok(format!("{repository_path}/{}", service.as_str()))
1127}
1128
1129pub fn dumb_http_info_refs_path(repository_path: &str) -> Result<String> {
1130    let repository_path = normalize_http_repository_path(repository_path)?;
1131    Ok(format!("{repository_path}/info/refs"))
1132}
1133
1134pub fn dumb_http_alternates_path(repository_path: &str) -> Result<String> {
1135    let repository_path = normalize_http_repository_path(repository_path)?;
1136    Ok(format!("{repository_path}/objects/info/http-alternates"))
1137}
1138
1139pub fn dumb_http_packs_path(repository_path: &str) -> Result<String> {
1140    let repository_path = normalize_http_repository_path(repository_path)?;
1141    Ok(format!("{repository_path}/objects/info/packs"))
1142}
1143
1144pub fn dumb_http_loose_object_path(repository_path: &str, oid: &ObjectId) -> Result<String> {
1145    let repository_path = normalize_http_repository_path(repository_path)?;
1146    let oid = oid.to_string();
1147    let (directory, file) = oid.split_at(2);
1148    Ok(format!("{repository_path}/objects/{directory}/{file}"))
1149}
1150
1151pub fn dumb_http_pack_file_path(repository_path: &str, hash: &ObjectId) -> Result<String> {
1152    dumb_http_pack_resource_path(repository_path, hash, "pack")
1153}
1154
1155pub fn dumb_http_pack_index_path(repository_path: &str, hash: &ObjectId) -> Result<String> {
1156    dumb_http_pack_resource_path(repository_path, hash, "idx")
1157}
1158
1159pub fn smart_http_advertisement_content_type(service: GitService) -> Result<String> {
1160    validate_smart_http_service(service)?;
1161    Ok(format!("application/x-{}-advertisement", service.as_str()))
1162}
1163
1164pub fn smart_http_rpc_request_content_type(service: GitService) -> Result<String> {
1165    validate_smart_http_service(service)?;
1166    Ok(format!("application/x-{}-request", service.as_str()))
1167}
1168
1169pub fn smart_http_rpc_result_content_type(service: GitService) -> Result<String> {
1170    validate_smart_http_service(service)?;
1171    Ok(format!("application/x-{}-result", service.as_str()))
1172}
1173
1174pub fn parse_smart_http_advertisement_content_type(value: &str) -> Result<GitService> {
1175    parse_smart_http_content_type(value, "-advertisement")
1176}
1177
1178pub fn parse_smart_http_rpc_request_content_type(value: &str) -> Result<GitService> {
1179    parse_smart_http_content_type(value, "-request")
1180}
1181
1182pub fn parse_smart_http_rpc_result_content_type(value: &str) -> Result<GitService> {
1183    parse_smart_http_content_type(value, "-result")
1184}
1185
1186pub fn parse_sideband_packet(payload: &[u8]) -> Result<SideBandPacket> {
1187    let Some((&channel, data)) = payload.split_first() else {
1188        return Err(GitError::InvalidFormat("sideband packet is empty".into()));
1189    };
1190    let channel = match channel {
1191        1 => SideBandChannel::Data,
1192        2 => SideBandChannel::Progress,
1193        3 => SideBandChannel::Fatal,
1194        other => {
1195            return Err(GitError::InvalidFormat(format!(
1196                "invalid sideband channel {other}"
1197            )));
1198        }
1199    };
1200    Ok(SideBandPacket {
1201        channel,
1202        data: data.to_vec(),
1203    })
1204}
1205
1206pub fn encode_sideband_packet(packet: &SideBandPacket) -> Result<Vec<u8>> {
1207    let mut out = Vec::with_capacity(packet.data.len() + 1);
1208    out.push(match packet.channel {
1209        SideBandChannel::Data => 1,
1210        SideBandChannel::Progress => 2,
1211        SideBandChannel::Fatal => 3,
1212    });
1213    out.extend_from_slice(&packet.data);
1214    if out.len() > PKT_LINE_MAX_PAYLOAD_LEN {
1215        return Err(GitError::InvalidFormat(format!(
1216            "sideband packet exceeds {PKT_LINE_MAX_PAYLOAD_LEN} bytes"
1217        )));
1218    }
1219    Ok(out)
1220}
1221
1222pub fn write_sideband_packet(writer: &mut impl Write, packet: &SideBandPacket) -> Result<()> {
1223    write_sideband_payload(writer, packet.channel, &packet.data)
1224}
1225
1226fn write_sideband_payload(
1227    writer: &mut impl Write,
1228    channel: SideBandChannel,
1229    data: &[u8],
1230) -> Result<()> {
1231    let payload_len = data
1232        .len()
1233        .checked_add(1)
1234        .ok_or_else(|| GitError::InvalidFormat("sideband packet length overflow".into()))?;
1235    if payload_len > PKT_LINE_MAX_PAYLOAD_LEN {
1236        return Err(GitError::InvalidFormat(format!(
1237            "sideband packet exceeds {PKT_LINE_MAX_PAYLOAD_LEN} bytes"
1238        )));
1239    }
1240    writer.write_all(&pkt_line_header(payload_len + 4))?;
1241    writer.write_all(&[match channel {
1242        SideBandChannel::Data => 1,
1243        SideBandChannel::Progress => 2,
1244        SideBandChannel::Fatal => 3,
1245    }])?;
1246    writer.write_all(data)?;
1247    Ok(())
1248}
1249
1250pub fn parse_sideband_packets(payloads: &[Vec<u8>]) -> Result<Vec<SideBandPacket>> {
1251    payloads
1252        .iter()
1253        .map(|payload| parse_sideband_packet(payload))
1254        .collect()
1255}
1256
1257pub fn encode_sideband_packets(packets: &[SideBandPacket]) -> Result<Vec<Vec<u8>>> {
1258    packets.iter().map(encode_sideband_packet).collect()
1259}
1260
1261pub fn parse_sideband_stream(frames: &[PktLineFrame]) -> Result<Vec<SideBandPacket>> {
1262    let mut packets = Vec::new();
1263    let mut saw_flush = false;
1264    for (idx, frame) in frames.iter().enumerate() {
1265        match frame {
1266            PktLineFrame::Data(payload) if !saw_flush => {
1267                packets.push(parse_sideband_packet(payload)?);
1268            }
1269            PktLineFrame::Data(_) => {
1270                return Err(GitError::InvalidFormat(
1271                    "sideband stream has data after flush".into(),
1272                ));
1273            }
1274            PktLineFrame::Flush => {
1275                saw_flush = true;
1276                if idx + 1 != frames.len() {
1277                    return Err(GitError::InvalidFormat(
1278                        "sideband stream has frames after flush".into(),
1279                    ));
1280                }
1281            }
1282            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
1283                return Err(GitError::InvalidFormat(
1284                    "sideband stream contains a non-flush control packet".into(),
1285                ));
1286            }
1287        }
1288    }
1289    if !saw_flush {
1290        return Err(GitError::InvalidFormat(
1291            "sideband stream missing flush".into(),
1292        ));
1293    }
1294    Ok(packets)
1295}
1296
1297pub fn encode_sideband_stream(packets: &[SideBandPacket]) -> Result<Vec<PktLineFrame>> {
1298    let mut frames = Vec::new();
1299    for packet in packets {
1300        frames.push(PktLineFrame::data(encode_sideband_packet(packet)?)?);
1301    }
1302    frames.push(PktLineFrame::Flush);
1303    Ok(frames)
1304}
1305
1306pub fn read_sideband_stream(reader: &mut impl Read) -> Result<Vec<SideBandPacket>> {
1307    let frames = read_pkt_line_frames_until_flush(reader)?;
1308    parse_sideband_stream(&frames)
1309}
1310
1311pub fn write_sideband_stream(writer: &mut impl Write, packets: &[SideBandPacket]) -> Result<()> {
1312    for packet in packets {
1313        write_sideband_packet(writer, packet)?;
1314    }
1315    writer.write_all(b"0000")?;
1316    Ok(())
1317}
1318
1319pub fn demux_sideband_packets(packets: &[SideBandPacket]) -> Result<SideBandDemux> {
1320    let mut out = SideBandDemux::default();
1321    for packet in packets {
1322        match packet.channel {
1323            SideBandChannel::Data => out.data.extend_from_slice(&packet.data),
1324            SideBandChannel::Progress => out.progress.push(packet.data.clone()),
1325            SideBandChannel::Fatal => {
1326                let message = String::from_utf8_lossy(&packet.data).into_owned();
1327                return Err(GitError::InvalidFormat(format!(
1328                    "sideband fatal: {message}"
1329                )));
1330            }
1331        }
1332    }
1333    Ok(out)
1334}
1335
1336pub fn parse_and_demux_sideband_packets(payloads: &[Vec<u8>]) -> Result<SideBandDemux> {
1337    let packets = parse_sideband_packets(payloads)?;
1338    demux_sideband_packets(&packets)
1339}
1340
1341pub fn demux_sideband_stream(frames: &[PktLineFrame]) -> Result<SideBandDemux> {
1342    let packets = parse_sideband_stream(frames)?;
1343    demux_sideband_packets(&packets)
1344}
1345
1346pub fn read_and_demux_sideband_stream(reader: &mut impl Read) -> Result<SideBandDemux> {
1347    let packets = read_sideband_stream(reader)?;
1348    demux_sideband_packets(&packets)
1349}
1350
1351pub fn parse_upload_archive_request(frames: &[PktLineFrame]) -> Result<UploadArchiveRequest> {
1352    let mut request = UploadArchiveRequest::default();
1353    let mut saw_flush = false;
1354    for (idx, frame) in frames.iter().enumerate() {
1355        match frame {
1356            PktLineFrame::Data(payload) if !saw_flush => {
1357                let text = parse_protocol_v2_line_text("upload-archive request argument", payload)?;
1358                let argument = text.strip_prefix("argument ").ok_or_else(|| {
1359                    GitError::InvalidFormat("upload-archive request line must be argument".into())
1360                })?;
1361                validate_upload_archive_argument(argument)?;
1362                request.arguments.push(argument.to_string());
1363            }
1364            PktLineFrame::Data(_) => {
1365                return Err(GitError::InvalidFormat(
1366                    "upload-archive request has data after flush".into(),
1367                ));
1368            }
1369            PktLineFrame::Flush => {
1370                saw_flush = true;
1371                if idx + 1 != frames.len() {
1372                    return Err(GitError::InvalidFormat(
1373                        "upload-archive request has frames after flush".into(),
1374                    ));
1375                }
1376            }
1377            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
1378                return Err(GitError::InvalidFormat(
1379                    "upload-archive request contains a non-flush control packet".into(),
1380                ));
1381            }
1382        }
1383    }
1384    if !saw_flush {
1385        return Err(GitError::InvalidFormat(
1386            "upload-archive request missing flush".into(),
1387        ));
1388    }
1389    if request.arguments.is_empty() {
1390        return Err(GitError::InvalidFormat(
1391            "upload-archive request is missing arguments".into(),
1392        ));
1393    }
1394    Ok(request)
1395}
1396
1397pub fn encode_upload_archive_request(request: &UploadArchiveRequest) -> Result<Vec<PktLineFrame>> {
1398    if request.arguments.is_empty() {
1399        return Err(GitError::InvalidFormat(
1400            "upload-archive request is missing arguments".into(),
1401        ));
1402    }
1403    let mut frames = Vec::new();
1404    for argument in &request.arguments {
1405        validate_upload_archive_argument(argument)?;
1406        frames.push(PktLineFrame::data(line_from_str(&format!(
1407            "argument {argument}"
1408        )))?);
1409    }
1410    frames.push(PktLineFrame::Flush);
1411    Ok(frames)
1412}
1413
1414pub fn read_upload_archive_request(reader: &mut impl Read) -> Result<UploadArchiveRequest> {
1415    let frames = read_pkt_line_frames_until_flush(reader)?;
1416    parse_upload_archive_request(&frames)
1417}
1418
1419pub fn write_upload_archive_request(
1420    writer: &mut impl Write,
1421    request: &UploadArchiveRequest,
1422) -> Result<()> {
1423    if request.arguments.is_empty() {
1424        return Err(GitError::InvalidFormat(
1425            "upload-archive request is missing arguments".into(),
1426        ));
1427    }
1428    for argument in &request.arguments {
1429        validate_upload_archive_argument(argument)?;
1430        write_pkt_line_payload(writer, &line_from_str(&format!("argument {argument}")))?;
1431    }
1432    writer.write_all(b"0000")?;
1433    Ok(())
1434}
1435
1436pub fn parse_upload_archive_response(frames: &[PktLineFrame]) -> Result<UploadArchiveResponse> {
1437    let Some((first, rest)) = frames.split_first() else {
1438        return Err(GitError::InvalidFormat(
1439            "upload-archive response is empty".into(),
1440        ));
1441    };
1442    let PktLineFrame::Data(payload) = first else {
1443        return Err(GitError::InvalidFormat(
1444            "upload-archive response must start with a data packet".into(),
1445        ));
1446    };
1447    let text = parse_protocol_v2_line_text("upload-archive response status", payload)?;
1448    if text == "ACK" {
1449        return Ok(UploadArchiveResponse::Ack {
1450            sideband: parse_sideband_stream(rest)?,
1451        });
1452    }
1453    if let Some(message) = text.strip_prefix("NACK ") {
1454        validate_upload_archive_status_message(message)?;
1455        if !matches!(rest, [PktLineFrame::Flush]) {
1456            return Err(GitError::InvalidFormat(
1457                "upload-archive NACK response must end with flush".into(),
1458            ));
1459        }
1460        return Ok(UploadArchiveResponse::Nack {
1461            message: message.to_string(),
1462        });
1463    }
1464    Err(GitError::InvalidFormat(format!(
1465        "unsupported upload-archive response status {text}"
1466    )))
1467}
1468
1469pub fn encode_upload_archive_response(
1470    response: &UploadArchiveResponse,
1471) -> Result<Vec<PktLineFrame>> {
1472    let mut frames = Vec::new();
1473    match response {
1474        UploadArchiveResponse::Ack { sideband } => {
1475            frames.push(PktLineFrame::data(line_from_str("ACK"))?);
1476            frames.extend(encode_sideband_stream(sideband)?);
1477        }
1478        UploadArchiveResponse::Nack { message } => {
1479            validate_upload_archive_status_message(message)?;
1480            frames.push(PktLineFrame::data(line_from_str(&format!(
1481                "NACK {message}"
1482            )))?);
1483            frames.push(PktLineFrame::Flush);
1484        }
1485    }
1486    Ok(frames)
1487}
1488
1489pub fn read_upload_archive_response(reader: &mut impl Read) -> Result<UploadArchiveResponse> {
1490    let frames = read_pkt_line_frames_until_flush(reader)?;
1491    parse_upload_archive_response(&frames)
1492}
1493
1494pub fn write_upload_archive_response(
1495    writer: &mut impl Write,
1496    response: &UploadArchiveResponse,
1497) -> Result<()> {
1498    match response {
1499        UploadArchiveResponse::Ack { sideband } => {
1500            write_pkt_line_payload(writer, b"ACK\n")?;
1501            write_sideband_stream(writer, sideband)?;
1502        }
1503        UploadArchiveResponse::Nack { message } => {
1504            validate_upload_archive_status_message(message)?;
1505            write_pkt_line_payload(writer, &line_from_str(&format!("NACK {message}")))?;
1506            writer.write_all(b"0000")?;
1507        }
1508    }
1509    Ok(())
1510}
1511
1512pub fn demux_upload_archive_response(response: &UploadArchiveResponse) -> Result<SideBandDemux> {
1513    match response {
1514        UploadArchiveResponse::Ack { sideband } => demux_sideband_packets(sideband),
1515        UploadArchiveResponse::Nack { message } => Err(GitError::InvalidFormat(format!(
1516            "upload-archive NACK: {message}"
1517        ))),
1518    }
1519}
1520
1521fn parse_pkt_len(bytes: &[u8]) -> Result<usize> {
1522    let mut len = 0usize;
1523    for byte in bytes {
1524        len = (len << 4) | hex_nibble(*byte)? as usize;
1525    }
1526    Ok(len)
1527}
1528
1529fn hex_nibble(byte: u8) -> Result<u8> {
1530    match byte {
1531        b'0'..=b'9' => Ok(byte - b'0'),
1532        b'a'..=b'f' => Ok(byte - b'a' + 10),
1533        b'A'..=b'F' => Ok(byte - b'A' + 10),
1534        _ => Err(GitError::InvalidFormat(format!(
1535            "invalid pkt-line length byte {byte:#04x}"
1536        ))),
1537    }
1538}
1539
1540#[derive(Debug, Clone, PartialEq, Eq)]
1541pub struct TransportHandshake {
1542    pub protocol: ProtocolVersion,
1543    pub capabilities: Vec<Capability>,
1544}
1545
1546#[derive(Debug, Clone, PartialEq, Eq)]
1547pub struct RefAdvertisement {
1548    pub oid: ObjectId,
1549    pub name: String,
1550    pub capabilities: Vec<Capability>,
1551}
1552
1553#[derive(Debug, Clone, PartialEq, Eq)]
1554pub struct DumbHttpRefRecord {
1555    pub oid: ObjectId,
1556    pub name: String,
1557    pub peeled: bool,
1558}
1559
1560#[derive(Debug, Clone, PartialEq, Eq)]
1561pub struct DumbHttpPackRecord {
1562    pub hash: ObjectId,
1563}
1564
1565#[derive(Debug, Clone, PartialEq, Eq)]
1566pub struct RefAdvertisementSet {
1567    pub protocol: ProtocolVersion,
1568    pub refs: Vec<RefAdvertisement>,
1569    pub shallow: Vec<ObjectId>,
1570}
1571
1572#[derive(Debug, Clone, PartialEq, Eq, Default)]
1573pub struct UploadPackRequest {
1574    pub wants: Vec<ObjectId>,
1575    pub capabilities: Vec<Capability>,
1576    pub shallow: Vec<ObjectId>,
1577    pub deepen: Option<u32>,
1578    pub deepen_since: Option<u64>,
1579    pub deepen_not: Vec<String>,
1580    pub filter: Option<String>,
1581}
1582
1583#[derive(Debug, Clone, PartialEq, Eq, Default)]
1584pub struct UploadPackFeatures {
1585    pub multi_ack: bool,
1586    pub multi_ack_detailed: bool,
1587    pub no_done: bool,
1588    pub thin_pack: bool,
1589    pub side_band: bool,
1590    pub side_band_64k: bool,
1591    pub ofs_delta: bool,
1592    pub shallow: bool,
1593    pub deepen_since: bool,
1594    pub deepen_not: bool,
1595    pub include_tag: bool,
1596    pub no_progress: bool,
1597    pub allow_tip_sha1_in_want: bool,
1598    pub allow_reachable_sha1_in_want: bool,
1599    pub filter: bool,
1600    pub agent: Option<String>,
1601    pub object_format: Option<ObjectFormat>,
1602    pub symrefs: Vec<String>,
1603    pub unknown: Vec<Capability>,
1604}
1605
1606#[derive(Debug, Clone, PartialEq, Eq, Default)]
1607pub struct UploadPackNegotiationRequest {
1608    pub haves: Vec<ObjectId>,
1609    pub done: bool,
1610}
1611
1612#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1613pub enum UploadPackAckStatus {
1614    Continue,
1615    Common,
1616    Ready,
1617}
1618
1619#[derive(Debug, Clone, PartialEq, Eq)]
1620pub enum UploadPackAcknowledgment {
1621    Nak,
1622    Ack {
1623        oid: ObjectId,
1624        status: Option<UploadPackAckStatus>,
1625    },
1626}
1627
1628#[derive(Debug, Clone, PartialEq, Eq, Default)]
1629pub struct UploadPackPackfileResponse {
1630    pub acknowledgments: Vec<UploadPackAcknowledgment>,
1631    pub sideband: Vec<SideBandPacket>,
1632}
1633
1634#[derive(Debug, Clone, PartialEq, Eq, Default)]
1635pub struct UploadPackRawPackfileResponse {
1636    pub acknowledgments: Vec<UploadPackAcknowledgment>,
1637    pub packfile: Vec<u8>,
1638}
1639
1640#[derive(Debug, Clone, PartialEq, Eq, Default)]
1641pub struct UploadPackRawPackfileResponseHeader {
1642    pub acknowledgments: Vec<UploadPackAcknowledgment>,
1643    pub pack_prefix: Vec<u8>,
1644}
1645
1646#[derive(Debug, Clone, PartialEq, Eq)]
1647pub struct ReceivePackCommand {
1648    pub old_id: ObjectId,
1649    pub new_id: ObjectId,
1650    pub name: String,
1651}
1652
1653#[derive(Debug, Clone, PartialEq, Eq, Default)]
1654pub struct ReceivePackRequest {
1655    pub shallow: Vec<ObjectId>,
1656    pub commands: Vec<ReceivePackCommand>,
1657    pub capabilities: Vec<Capability>,
1658}
1659
1660#[derive(Debug, Clone, PartialEq, Eq, Default)]
1661pub struct ReceivePackPushRequest {
1662    pub commands: ReceivePackRequest,
1663    pub push_options: Option<Vec<String>>,
1664    pub packfile: Vec<u8>,
1665}
1666
1667#[derive(Debug, Clone, PartialEq, Eq, Default)]
1668pub struct ReceivePackPushRequestHeader {
1669    pub commands: ReceivePackRequest,
1670    pub push_options: Option<Vec<String>>,
1671}
1672
1673#[derive(Debug, Clone, PartialEq, Eq, Default)]
1674pub struct ReceivePackPushRequestOptions {
1675    pub report_status: bool,
1676    pub report_status_v2: bool,
1677    pub atomic: bool,
1678    pub ofs_delta: bool,
1679    pub side_band_64k: bool,
1680    pub quiet: bool,
1681    pub agent: Option<String>,
1682    pub object_format: Option<ObjectFormat>,
1683    pub push_options: Vec<String>,
1684}
1685
1686#[derive(Debug, Clone, PartialEq, Eq, Default)]
1687pub struct ReceivePackFeatures {
1688    pub report_status: bool,
1689    pub report_status_v2: bool,
1690    pub delete_refs: bool,
1691    pub ofs_delta: bool,
1692    pub atomic: bool,
1693    pub push_options: bool,
1694    pub side_band_64k: bool,
1695    pub quiet: bool,
1696    pub no_thin: bool,
1697    pub agent: Option<String>,
1698    pub object_format: Option<ObjectFormat>,
1699    pub unknown: Vec<Capability>,
1700}
1701
1702#[derive(Debug, Clone, PartialEq, Eq)]
1703pub enum ReceivePackUnpackStatus {
1704    Ok,
1705    Error(String),
1706}
1707
1708#[derive(Debug, Clone, PartialEq, Eq)]
1709pub enum ReceivePackCommandStatus {
1710    Ok { name: String },
1711    Ng { name: String, message: String },
1712}
1713
1714#[derive(Debug, Clone, PartialEq, Eq)]
1715pub struct ReceivePackReportStatus {
1716    pub unpack: ReceivePackUnpackStatus,
1717    pub commands: Vec<ReceivePackCommandStatus>,
1718}
1719
1720#[derive(Debug, Clone, PartialEq, Eq, Default)]
1721pub struct ReceivePackCommandStatusV2Options {
1722    pub refname: Option<String>,
1723    pub old_oid: Option<ObjectId>,
1724    pub new_oid: Option<ObjectId>,
1725    pub forced_update: bool,
1726}
1727
1728#[derive(Debug, Clone, PartialEq, Eq)]
1729pub enum ReceivePackCommandStatusV2 {
1730    Ok {
1731        name: String,
1732        options: ReceivePackCommandStatusV2Options,
1733    },
1734    Ng {
1735        name: String,
1736        message: String,
1737    },
1738}
1739
1740#[derive(Debug, Clone, PartialEq, Eq)]
1741pub struct ReceivePackReportStatusV2 {
1742    pub unpack: ReceivePackUnpackStatus,
1743    pub commands: Vec<ReceivePackCommandStatusV2>,
1744}
1745
1746#[derive(Debug, Clone, PartialEq, Eq)]
1747pub struct ProtocolV2CommandRequest {
1748    pub command: String,
1749    pub capabilities: Vec<Capability>,
1750    pub arguments: Vec<Vec<u8>>,
1751}
1752
1753#[derive(Debug, Clone, PartialEq, Eq)]
1754pub enum ProtocolV2Request {
1755    Command(ProtocolV2CommandRequest),
1756    Done,
1757}
1758
1759#[derive(Debug, Clone, PartialEq, Eq)]
1760pub enum ProtocolV2Command {
1761    LsRefs(ProtocolV2LsRefsRequest),
1762    Fetch(ProtocolV2FetchRequest),
1763    ObjectInfo(ProtocolV2ObjectInfoRequest),
1764    Unknown(ProtocolV2CommandRequest),
1765}
1766
1767#[derive(Debug, Clone, PartialEq, Eq)]
1768pub enum ProtocolV2SessionRequest {
1769    Command(ProtocolV2Command),
1770    Done,
1771}
1772
1773#[derive(Debug, Clone, PartialEq, Eq, Default)]
1774pub struct ProtocolV2CommandOptions {
1775    pub agent: Option<String>,
1776    pub object_format: Option<ObjectFormat>,
1777    pub server_options: Vec<String>,
1778    pub extra: Vec<Capability>,
1779}
1780
1781#[derive(Debug, Clone, PartialEq, Eq, Default)]
1782pub struct ProtocolV2FetchFeatures {
1783    pub shallow: bool,
1784    pub wait_for_done: bool,
1785    pub filter: bool,
1786    pub ref_in_want: bool,
1787    pub sideband_all: bool,
1788    pub packfile_uris: bool,
1789    pub unknown: Vec<String>,
1790}
1791
1792#[derive(Debug, Clone, PartialEq, Eq, Default)]
1793pub struct ProtocolV2LsRefsFeatures {
1794    pub unborn: bool,
1795    pub unknown: Vec<String>,
1796}
1797
1798impl ProtocolV2CommandRequest {
1799    pub fn new(command: impl Into<String>) -> Result<Self> {
1800        let command = command.into();
1801        validate_capability_name(&command)?;
1802        Ok(Self {
1803            command,
1804            capabilities: Vec::new(),
1805            arguments: Vec::new(),
1806        })
1807    }
1808}
1809
1810#[derive(Debug, Clone, PartialEq, Eq, Default)]
1811pub struct ProtocolV2LsRefsRequest {
1812    pub peel: bool,
1813    pub symrefs: bool,
1814    pub unborn: bool,
1815    pub ref_prefixes: Vec<String>,
1816}
1817
1818#[derive(Debug, Clone, PartialEq, Eq)]
1819pub struct ProtocolV2LsRefsRef {
1820    pub oid: ObjectId,
1821    pub name: String,
1822    pub peeled: Option<ObjectId>,
1823    pub symref_target: Option<String>,
1824    pub attributes: Vec<String>,
1825}
1826
1827#[derive(Debug, Clone, PartialEq, Eq)]
1828pub enum ProtocolV2LsRefsRecord {
1829    Ref(ProtocolV2LsRefsRef),
1830    Unborn {
1831        name: String,
1832        symref_target: Option<String>,
1833        attributes: Vec<String>,
1834    },
1835}
1836
1837#[derive(Debug, Clone, PartialEq, Eq, Default)]
1838pub struct ProtocolV2FetchRequest {
1839    pub wants: Vec<ObjectId>,
1840    pub want_refs: Vec<String>,
1841    pub haves: Vec<ObjectId>,
1842    pub shallow: Vec<ObjectId>,
1843    pub deepen: Option<u32>,
1844    pub deepen_since: Option<u64>,
1845    pub deepen_not: Vec<String>,
1846    pub deepen_relative: bool,
1847    pub filter: Option<String>,
1848    pub packfile_uris: Option<String>,
1849    pub thin_pack: bool,
1850    pub no_progress: bool,
1851    pub include_tag: bool,
1852    pub ofs_delta: bool,
1853    pub sideband_all: bool,
1854    pub wait_for_done: bool,
1855    pub done: bool,
1856}
1857
1858#[derive(Debug, Clone, PartialEq, Eq)]
1859pub enum ProtocolV2FetchAcknowledgment {
1860    Nak,
1861    Ack(ObjectId),
1862    Ready,
1863}
1864
1865#[derive(Debug, Clone, PartialEq, Eq)]
1866pub enum ProtocolV2FetchShallowInfo {
1867    Shallow(ObjectId),
1868    Unshallow(ObjectId),
1869}
1870
1871#[derive(Debug, Clone, PartialEq, Eq)]
1872pub struct ProtocolV2FetchWantedRef {
1873    pub oid: ObjectId,
1874    pub name: String,
1875}
1876
1877#[derive(Debug, Clone, PartialEq, Eq)]
1878pub struct ProtocolV2FetchPackfileUri {
1879    pub pack_hash: ObjectId,
1880    pub uri: String,
1881}
1882
1883#[derive(Debug, Clone, PartialEq, Eq)]
1884pub enum ProtocolV2FetchResponseSection {
1885    Acknowledgments(Vec<ProtocolV2FetchAcknowledgment>),
1886    ShallowInfo(Vec<ProtocolV2FetchShallowInfo>),
1887    WantedRefs(Vec<ProtocolV2FetchWantedRef>),
1888    PackfileUris(Vec<ProtocolV2FetchPackfileUri>),
1889    Packfile(Vec<Vec<u8>>),
1890    Unknown { name: String, lines: Vec<Vec<u8>> },
1891}
1892
1893#[derive(Debug, Clone, PartialEq, Eq, Default)]
1894pub struct ProtocolV2FetchSidebandAllResponse {
1895    pub sections: Vec<ProtocolV2FetchResponseSection>,
1896    pub progress: Vec<Vec<u8>>,
1897}
1898
1899#[derive(Debug, Clone, PartialEq, Eq, Default)]
1900pub struct ProtocolV2FetchResponseHeader {
1901    pub sections: Vec<ProtocolV2FetchResponseSection>,
1902    pub has_packfile: bool,
1903}
1904
1905#[derive(Debug, Clone, PartialEq, Eq, Default)]
1906pub struct ProtocolV2ObjectInfoRequest {
1907    pub size: bool,
1908    pub oids: Vec<ObjectId>,
1909}
1910
1911#[derive(Debug, Clone, PartialEq, Eq)]
1912pub struct ProtocolV2ObjectInfoRecord {
1913    pub oid: ObjectId,
1914    pub size: u64,
1915}
1916
1917#[derive(Debug, Clone, PartialEq, Eq, Default)]
1918pub struct ProtocolV2ObjectInfoResponse {
1919    pub size: bool,
1920    pub records: Vec<ProtocolV2ObjectInfoRecord>,
1921}
1922
1923impl ProtocolV2LsRefsRequest {
1924    pub fn from_command_request(request: &ProtocolV2CommandRequest) -> Result<Self> {
1925        if request.command != "ls-refs" {
1926            return Err(GitError::InvalidFormat(format!(
1927                "expected ls-refs command, got {}",
1928                request.command
1929            )));
1930        }
1931        let mut out = Self::default();
1932        for argument in &request.arguments {
1933            let text = std::str::from_utf8(argument)
1934                .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
1935            match text {
1936                "peel" => out.peel = true,
1937                "symrefs" => out.symrefs = true,
1938                "unborn" => out.unborn = true,
1939                value if value.starts_with("ref-prefix ") => {
1940                    let prefix = value
1941                        .strip_prefix("ref-prefix ")
1942                        .ok_or_else(|| GitError::InvalidFormat("invalid ref-prefix".into()))?;
1943                    validate_protocol_v2_token("ls-refs ref-prefix", prefix)?;
1944                    out.ref_prefixes.push(prefix.to_string());
1945                }
1946                other => {
1947                    return Err(GitError::InvalidFormat(format!(
1948                        "unsupported ls-refs argument {other}"
1949                    )));
1950                }
1951            }
1952        }
1953        Ok(out)
1954    }
1955
1956    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
1957        let mut request = ProtocolV2CommandRequest::new("ls-refs")?;
1958        if self.peel {
1959            request.arguments.push(b"peel".to_vec());
1960        }
1961        if self.symrefs {
1962            request.arguments.push(b"symrefs".to_vec());
1963        }
1964        if self.unborn {
1965            request.arguments.push(b"unborn".to_vec());
1966        }
1967        for prefix in &self.ref_prefixes {
1968            validate_protocol_v2_token("ls-refs ref-prefix", prefix)?;
1969            request
1970                .arguments
1971                .push(format!("ref-prefix {prefix}").into_bytes());
1972        }
1973        Ok(request)
1974    }
1975}
1976
1977impl ProtocolV2FetchRequest {
1978    pub fn from_command_request(
1979        format: ObjectFormat,
1980        request: &ProtocolV2CommandRequest,
1981    ) -> Result<Self> {
1982        if request.command != "fetch" {
1983            return Err(GitError::InvalidFormat(format!(
1984                "expected fetch command, got {}",
1985                request.command
1986            )));
1987        }
1988        let mut out = Self::default();
1989        for argument in &request.arguments {
1990            let text = std::str::from_utf8(argument)
1991                .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
1992            match text {
1993                "thin-pack" => out.thin_pack = true,
1994                "no-progress" => out.no_progress = true,
1995                "include-tag" => out.include_tag = true,
1996                "ofs-delta" => out.ofs_delta = true,
1997                "sideband-all" => out.sideband_all = true,
1998                "wait-for-done" => out.wait_for_done = true,
1999                "deepen-relative" => out.deepen_relative = true,
2000                "done" => out.done = true,
2001                value if value.starts_with("want ") => {
2002                    out.wants
2003                        .push(parse_oid_argument(format, "fetch want", value, "want ")?);
2004                }
2005                value if value.starts_with("want-ref ") => {
2006                    let name = value
2007                        .strip_prefix("want-ref ")
2008                        .ok_or_else(|| GitError::InvalidFormat("invalid fetch want-ref".into()))?;
2009                    validate_protocol_v2_token("fetch want-ref", name)?;
2010                    out.want_refs.push(name.to_string());
2011                }
2012                value if value.starts_with("have ") => {
2013                    out.haves
2014                        .push(parse_oid_argument(format, "fetch have", value, "have ")?);
2015                }
2016                value if value.starts_with("shallow ") => {
2017                    out.shallow.push(parse_oid_argument(
2018                        format,
2019                        "fetch shallow",
2020                        value,
2021                        "shallow ",
2022                    )?);
2023                }
2024                value if value.starts_with("deepen ") => {
2025                    if out.deepen.is_some() {
2026                        return Err(GitError::InvalidFormat(
2027                            "fetch request has duplicate deepen".into(),
2028                        ));
2029                    }
2030                    out.deepen = Some(parse_u32_argument("fetch deepen", value, "deepen ")?);
2031                }
2032                value if value.starts_with("deepen-since ") => {
2033                    if out.deepen_since.is_some() {
2034                        return Err(GitError::InvalidFormat(
2035                            "fetch request has duplicate deepen-since".into(),
2036                        ));
2037                    }
2038                    out.deepen_since = Some(parse_u64_argument(
2039                        "fetch deepen-since",
2040                        value,
2041                        "deepen-since ",
2042                    )?);
2043                }
2044                value if value.starts_with("deepen-not ") => {
2045                    let name = value.strip_prefix("deepen-not ").ok_or_else(|| {
2046                        GitError::InvalidFormat("invalid fetch deepen-not".into())
2047                    })?;
2048                    validate_protocol_v2_token("fetch deepen-not", name)?;
2049                    out.deepen_not.push(name.to_string());
2050                }
2051                value if value.starts_with("filter ") => {
2052                    if out.filter.is_some() {
2053                        return Err(GitError::InvalidFormat(
2054                            "fetch request has duplicate filter".into(),
2055                        ));
2056                    }
2057                    let filter = value
2058                        .strip_prefix("filter ")
2059                        .ok_or_else(|| GitError::InvalidFormat("invalid fetch filter".into()))?;
2060                    validate_protocol_v2_token("fetch filter", filter)?;
2061                    out.filter = Some(filter.to_string());
2062                }
2063                value if value.starts_with("packfile-uris ") => {
2064                    if out.packfile_uris.is_some() {
2065                        return Err(GitError::InvalidFormat(
2066                            "fetch request has duplicate packfile-uris".into(),
2067                        ));
2068                    }
2069                    let protocols = value.strip_prefix("packfile-uris ").ok_or_else(|| {
2070                        GitError::InvalidFormat("invalid fetch packfile-uris".into())
2071                    })?;
2072                    validate_protocol_v2_token("fetch packfile-uris", protocols)?;
2073                    out.packfile_uris = Some(protocols.to_string());
2074                }
2075                other => {
2076                    return Err(GitError::InvalidFormat(format!(
2077                        "unsupported fetch argument {other}"
2078                    )));
2079                }
2080            }
2081        }
2082        Ok(out)
2083    }
2084
2085    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
2086        let mut request = ProtocolV2CommandRequest::new("fetch")?;
2087        for oid in &self.wants {
2088            request.arguments.push(format!("want {oid}").into_bytes());
2089        }
2090        for name in &self.want_refs {
2091            validate_protocol_v2_token("fetch want-ref", name)?;
2092            request
2093                .arguments
2094                .push(format!("want-ref {name}").into_bytes());
2095        }
2096        for oid in &self.haves {
2097            request.arguments.push(format!("have {oid}").into_bytes());
2098        }
2099        for oid in &self.shallow {
2100            request
2101                .arguments
2102                .push(format!("shallow {oid}").into_bytes());
2103        }
2104        if let Some(deepen) = self.deepen {
2105            if deepen == 0 {
2106                return Err(GitError::InvalidFormat(
2107                    "fetch deepen must be positive".into(),
2108                ));
2109            }
2110            request
2111                .arguments
2112                .push(format!("deepen {deepen}").into_bytes());
2113        }
2114        if let Some(deepen_since) = self.deepen_since {
2115            request
2116                .arguments
2117                .push(format!("deepen-since {deepen_since}").into_bytes());
2118        }
2119        for name in &self.deepen_not {
2120            validate_protocol_v2_token("fetch deepen-not", name)?;
2121            request
2122                .arguments
2123                .push(format!("deepen-not {name}").into_bytes());
2124        }
2125        if self.deepen_relative {
2126            request.arguments.push(b"deepen-relative".to_vec());
2127        }
2128        if let Some(filter) = &self.filter {
2129            validate_protocol_v2_token("fetch filter", filter)?;
2130            request
2131                .arguments
2132                .push(format!("filter {filter}").into_bytes());
2133        }
2134        if let Some(protocols) = &self.packfile_uris {
2135            validate_protocol_v2_token("fetch packfile-uris", protocols)?;
2136            request
2137                .arguments
2138                .push(format!("packfile-uris {protocols}").into_bytes());
2139        }
2140        if self.thin_pack {
2141            request.arguments.push(b"thin-pack".to_vec());
2142        }
2143        if self.no_progress {
2144            request.arguments.push(b"no-progress".to_vec());
2145        }
2146        if self.include_tag {
2147            request.arguments.push(b"include-tag".to_vec());
2148        }
2149        if self.ofs_delta {
2150            request.arguments.push(b"ofs-delta".to_vec());
2151        }
2152        if self.sideband_all {
2153            request.arguments.push(b"sideband-all".to_vec());
2154        }
2155        if self.wait_for_done {
2156            request.arguments.push(b"wait-for-done".to_vec());
2157        }
2158        if self.done {
2159            request.arguments.push(b"done".to_vec());
2160        }
2161        Ok(request)
2162    }
2163}
2164
2165impl ProtocolV2ObjectInfoRequest {
2166    pub fn from_command_request(
2167        format: ObjectFormat,
2168        request: &ProtocolV2CommandRequest,
2169    ) -> Result<Self> {
2170        if request.command != "object-info" {
2171            return Err(GitError::InvalidFormat(format!(
2172                "expected object-info command, got {}",
2173                request.command
2174            )));
2175        }
2176        let mut out = Self::default();
2177        for argument in &request.arguments {
2178            let text = parse_protocol_v2_line_text("object-info request argument", argument)?;
2179            if text == "size" {
2180                if out.size {
2181                    return Err(GitError::InvalidFormat(
2182                        "object-info request has duplicate size argument".into(),
2183                    ));
2184                }
2185                out.size = true;
2186            } else if text.starts_with("oid ") {
2187                out.oids
2188                    .push(parse_oid_argument(format, "object-info oid", text, "oid ")?);
2189            } else {
2190                return Err(GitError::InvalidFormat(format!(
2191                    "unsupported object-info request argument {text}"
2192                )));
2193            }
2194        }
2195        if !out.size {
2196            return Err(GitError::InvalidFormat(
2197                "object-info request is missing size argument".into(),
2198            ));
2199        }
2200        if out.oids.is_empty() {
2201            return Err(GitError::InvalidFormat(
2202                "object-info request is missing object ids".into(),
2203            ));
2204        }
2205        Ok(out)
2206    }
2207
2208    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
2209        if !self.size {
2210            return Err(GitError::InvalidFormat(
2211                "object-info request is missing size argument".into(),
2212            ));
2213        }
2214        if self.oids.is_empty() {
2215            return Err(GitError::InvalidFormat(
2216                "object-info request is missing object ids".into(),
2217            ));
2218        }
2219        let mut request = ProtocolV2CommandRequest::new("object-info")?;
2220        request.arguments.push(b"size".to_vec());
2221        for oid in &self.oids {
2222            request.arguments.push(format!("oid {oid}").into_bytes());
2223        }
2224        Ok(request)
2225    }
2226}
2227
2228pub fn parse_protocol_v2_advertisement(frames: &[PktLineFrame]) -> Result<TransportHandshake> {
2229    let Some((first, rest)) = frames.split_first() else {
2230        return Err(GitError::InvalidFormat(
2231            "protocol v2 advertisement is empty".into(),
2232        ));
2233    };
2234    match first {
2235        PktLineFrame::Data(payload) if trim_trailing_lf(payload) == b"version 2" => {}
2236        PktLineFrame::Data(_) => {
2237            return Err(GitError::InvalidFormat(
2238                "protocol v2 advertisement missing version line".into(),
2239            ));
2240        }
2241        _ => {
2242            return Err(GitError::InvalidFormat(
2243                "protocol v2 advertisement must start with a data line".into(),
2244            ));
2245        }
2246    }
2247
2248    let mut capabilities = Vec::new();
2249    let mut saw_flush = false;
2250    for (idx, frame) in rest.iter().enumerate() {
2251        match frame {
2252            PktLineFrame::Data(payload) => {
2253                if saw_flush {
2254                    return Err(GitError::InvalidFormat(
2255                        "protocol v2 advertisement has data after flush".into(),
2256                    ));
2257                }
2258                capabilities.push(parse_protocol_v2_capability_line(payload)?);
2259            }
2260            PktLineFrame::Flush => {
2261                saw_flush = true;
2262                if idx + 1 != rest.len() {
2263                    return Err(GitError::InvalidFormat(
2264                        "protocol v2 advertisement has frames after flush".into(),
2265                    ));
2266                }
2267            }
2268            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
2269                return Err(GitError::InvalidFormat(
2270                    "protocol v2 advertisement contains a non-flush control packet".into(),
2271                ));
2272            }
2273        }
2274    }
2275    if !saw_flush {
2276        return Err(GitError::InvalidFormat(
2277            "protocol v2 advertisement missing flush".into(),
2278        ));
2279    }
2280
2281    Ok(TransportHandshake {
2282        protocol: ProtocolVersion::V2,
2283        capabilities,
2284    })
2285}
2286
2287pub fn encode_protocol_v2_advertisement(
2288    handshake: &TransportHandshake,
2289) -> Result<Vec<PktLineFrame>> {
2290    if handshake.protocol != ProtocolVersion::V2 {
2291        return Err(GitError::InvalidFormat(
2292            "protocol v2 advertisement requires a v2 handshake".into(),
2293        ));
2294    }
2295    let mut frames = vec![PktLineFrame::data(line_from_str("version 2"))?];
2296    for capability in &handshake.capabilities {
2297        frames.push(PktLineFrame::data(line(encode_protocol_v2_capability(
2298            capability,
2299        )?))?);
2300    }
2301    frames.push(PktLineFrame::Flush);
2302    Ok(frames)
2303}
2304
2305pub fn read_protocol_v2_advertisement(reader: &mut impl Read) -> Result<TransportHandshake> {
2306    let frames = read_pkt_line_frames_until_flush(reader)?;
2307    parse_protocol_v2_advertisement(&frames)
2308}
2309
2310pub fn write_protocol_v2_advertisement(
2311    writer: &mut impl Write,
2312    handshake: &TransportHandshake,
2313) -> Result<()> {
2314    if handshake.protocol != ProtocolVersion::V2 {
2315        return Err(GitError::InvalidFormat(
2316            "protocol v2 advertisement requires a v2 handshake".into(),
2317        ));
2318    }
2319    write_pkt_line_payload(writer, b"version 2\n")?;
2320    for capability in &handshake.capabilities {
2321        write_pkt_line_payload(writer, &line(encode_protocol_v2_capability(capability)?))?;
2322    }
2323    writer.write_all(b"0000")?;
2324    Ok(())
2325}
2326
2327pub fn parse_protocol_v2_command_request(
2328    frames: &[PktLineFrame],
2329) -> Result<ProtocolV2CommandRequest> {
2330    let Some((first, rest)) = frames.split_first() else {
2331        return Err(GitError::InvalidFormat(
2332            "protocol v2 command request is empty".into(),
2333        ));
2334    };
2335    let command = match first {
2336        PktLineFrame::Data(payload) => parse_protocol_v2_command_line(payload)?,
2337        _ => {
2338            return Err(GitError::InvalidFormat(
2339                "protocol v2 command request must start with a command line".into(),
2340            ));
2341        }
2342    };
2343
2344    let mut capabilities = Vec::new();
2345    let mut arguments = Vec::new();
2346    let mut in_arguments = false;
2347    let mut saw_flush = false;
2348    for (idx, frame) in rest.iter().enumerate() {
2349        match frame {
2350            PktLineFrame::Data(payload) if !in_arguments => {
2351                if saw_flush {
2352                    return Err(GitError::InvalidFormat(
2353                        "protocol v2 command request has data after flush".into(),
2354                    ));
2355                }
2356                capabilities.push(parse_protocol_v2_capability_line(payload)?);
2357            }
2358            PktLineFrame::Data(payload) => {
2359                if saw_flush {
2360                    return Err(GitError::InvalidFormat(
2361                        "protocol v2 command request has data after flush".into(),
2362                    ));
2363                }
2364                let argument = trim_trailing_lf(payload);
2365                if argument.is_empty() {
2366                    return Err(GitError::InvalidFormat(
2367                        "protocol v2 command argument is empty".into(),
2368                    ));
2369                }
2370                if argument
2371                    .iter()
2372                    .any(|byte| matches!(*byte, b'\n' | b'\r' | 0))
2373                {
2374                    return Err(GitError::InvalidFormat(
2375                        "protocol v2 command argument contains a delimiter byte".into(),
2376                    ));
2377                }
2378                arguments.push(argument.to_vec());
2379            }
2380            PktLineFrame::Delimiter => {
2381                if in_arguments {
2382                    return Err(GitError::InvalidFormat(format!(
2383                        "expected flush after {} arguments",
2384                        command
2385                    )));
2386                }
2387                if saw_flush {
2388                    return Err(GitError::InvalidFormat(
2389                        "protocol v2 command request has delimiter after flush".into(),
2390                    ));
2391                }
2392                in_arguments = true;
2393            }
2394            PktLineFrame::Flush => {
2395                saw_flush = true;
2396                if idx + 1 != rest.len() {
2397                    return Err(GitError::InvalidFormat(
2398                        "protocol v2 command request has frames after flush".into(),
2399                    ));
2400                }
2401            }
2402            PktLineFrame::ResponseEnd => {
2403                return Err(GitError::InvalidFormat(
2404                    "protocol v2 command request contains response-end".into(),
2405                ));
2406            }
2407        }
2408    }
2409    if !saw_flush {
2410        return Err(GitError::InvalidFormat(
2411            "protocol v2 command request missing flush".into(),
2412        ));
2413    }
2414
2415    Ok(ProtocolV2CommandRequest {
2416        command,
2417        capabilities,
2418        arguments,
2419    })
2420}
2421
2422pub fn encode_protocol_v2_command_request(
2423    request: &ProtocolV2CommandRequest,
2424) -> Result<Vec<PktLineFrame>> {
2425    validate_capability_name(&request.command)?;
2426    let mut frames = Vec::new();
2427    frames.push(PktLineFrame::data(line_from_str(&format!(
2428        "command={}",
2429        request.command
2430    )))?);
2431    for capability in &request.capabilities {
2432        frames.push(PktLineFrame::data(line(encode_protocol_v2_capability(
2433            capability,
2434        )?))?);
2435    }
2436    if !request.arguments.is_empty() {
2437        frames.push(PktLineFrame::Delimiter);
2438        for argument in &request.arguments {
2439            validate_protocol_v2_argument(argument)?;
2440            let mut payload = argument.clone();
2441            payload.push(b'\n');
2442            frames.push(PktLineFrame::data(payload)?);
2443        }
2444    }
2445    frames.push(PktLineFrame::Flush);
2446    Ok(frames)
2447}
2448
2449pub fn parse_protocol_v2_request(frames: &[PktLineFrame]) -> Result<ProtocolV2Request> {
2450    if matches!(frames, [PktLineFrame::Flush]) {
2451        return Ok(ProtocolV2Request::Done);
2452    }
2453    parse_protocol_v2_command_request(frames).map(ProtocolV2Request::Command)
2454}
2455
2456pub fn encode_protocol_v2_request(request: &ProtocolV2Request) -> Result<Vec<PktLineFrame>> {
2457    match request {
2458        ProtocolV2Request::Command(command) => encode_protocol_v2_command_request(command),
2459        ProtocolV2Request::Done => Ok(vec![PktLineFrame::Flush]),
2460    }
2461}
2462
2463pub fn read_protocol_v2_request(reader: &mut impl Read) -> Result<ProtocolV2Request> {
2464    let frames = read_pkt_line_frames_until_flush(reader)?;
2465    parse_protocol_v2_request(&frames)
2466}
2467
2468pub fn write_protocol_v2_request(
2469    writer: &mut impl Write,
2470    request: &ProtocolV2Request,
2471) -> Result<()> {
2472    match request {
2473        ProtocolV2Request::Command(command) => write_protocol_v2_command_request(writer, command),
2474        ProtocolV2Request::Done => {
2475            writer.write_all(b"0000")?;
2476            Ok(())
2477        }
2478    }
2479}
2480
2481pub fn read_protocol_v2_command_request(
2482    reader: &mut impl Read,
2483) -> Result<ProtocolV2CommandRequest> {
2484    let mut frames = Vec::new();
2485    loop {
2486        let Some(frame) = read_pkt_line_frame(reader)? else {
2487            if let Some(command) = frames.first().and_then(|frame| match frame {
2488                PktLineFrame::Data(payload) => parse_protocol_v2_command_line(payload).ok(),
2489                _ => None,
2490            }) && frames
2491                .iter()
2492                .any(|frame| matches!(frame, PktLineFrame::Delimiter))
2493            {
2494                return Err(GitError::InvalidFormat(format!(
2495                    "expected flush after {} arguments",
2496                    command
2497                )));
2498            }
2499            return Err(GitError::InvalidFormat(
2500                "pkt-line stream ended before control packet".into(),
2501            ));
2502        };
2503        let done = matches!(frame, PktLineFrame::Flush);
2504        frames.push(frame);
2505        if done {
2506            break;
2507        }
2508    }
2509    parse_protocol_v2_command_request(&frames)
2510}
2511
2512pub fn write_protocol_v2_command_request(
2513    writer: &mut impl Write,
2514    request: &ProtocolV2CommandRequest,
2515) -> Result<()> {
2516    validate_capability_name(&request.command)?;
2517    write_pkt_line_payload(
2518        writer,
2519        &line_from_str(&format!("command={}", request.command)),
2520    )?;
2521    for capability in &request.capabilities {
2522        write_pkt_line_payload(writer, &line(encode_protocol_v2_capability(capability)?))?;
2523    }
2524    if !request.arguments.is_empty() {
2525        write_pkt_line_frame(writer, &PktLineFrame::Delimiter)?;
2526        for argument in &request.arguments {
2527            validate_protocol_v2_argument(argument)?;
2528            let mut payload = argument.clone();
2529            payload.push(b'\n');
2530            write_pkt_line_payload(writer, &payload)?;
2531        }
2532    }
2533    writer.write_all(b"0000")?;
2534    Ok(())
2535}
2536
2537pub fn read_protocol_v2_ls_refs_request(reader: &mut impl Read) -> Result<ProtocolV2LsRefsRequest> {
2538    let request = read_protocol_v2_command_request(reader)?;
2539    ProtocolV2LsRefsRequest::from_command_request(&request)
2540}
2541
2542pub fn write_protocol_v2_ls_refs_request(
2543    writer: &mut impl Write,
2544    request: &ProtocolV2LsRefsRequest,
2545) -> Result<()> {
2546    let command = request.to_command_request()?;
2547    write_protocol_v2_command_request(writer, &command)
2548}
2549
2550pub fn parse_protocol_v2_ls_refs_response(
2551    format: ObjectFormat,
2552    frames: &[PktLineFrame],
2553) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2554    let mut records = Vec::new();
2555    let mut saw_flush = false;
2556    for (idx, frame) in frames.iter().enumerate() {
2557        match frame {
2558            PktLineFrame::Data(payload) => {
2559                if saw_flush {
2560                    return Err(GitError::InvalidFormat(
2561                        "ls-refs response has data after flush".into(),
2562                    ));
2563                }
2564                records.push(parse_protocol_v2_ls_refs_line(format, payload)?);
2565            }
2566            PktLineFrame::Flush => {
2567                saw_flush = true;
2568                if !flush_terminates_protocol_v2_response(frames, idx) {
2569                    return Err(GitError::InvalidFormat(
2570                        "ls-refs response has frames after flush".into(),
2571                    ));
2572                }
2573            }
2574            PktLineFrame::ResponseEnd if saw_flush && idx + 1 == frames.len() => {}
2575            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
2576                return Err(GitError::InvalidFormat(
2577                    "ls-refs response contains a non-flush control packet".into(),
2578                ));
2579            }
2580        }
2581    }
2582    if !saw_flush {
2583        return Err(GitError::InvalidFormat(
2584            "ls-refs response missing flush".into(),
2585        ));
2586    }
2587    Ok(records)
2588}
2589
2590pub fn encode_protocol_v2_ls_refs_response(
2591    records: &[ProtocolV2LsRefsRecord],
2592) -> Result<Vec<PktLineFrame>> {
2593    let mut frames = Vec::new();
2594    for record in records {
2595        frames.push(PktLineFrame::data(line_from_str(
2596            &format_protocol_v2_ls_refs_record(record)?,
2597        ))?);
2598    }
2599    frames.push(PktLineFrame::Flush);
2600    Ok(frames)
2601}
2602
2603pub fn read_protocol_v2_ls_refs_response(
2604    format: ObjectFormat,
2605    reader: &mut impl Read,
2606) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2607    let frames = read_pkt_line_frames_until_flush(reader)?;
2608    parse_protocol_v2_ls_refs_response(format, &frames)
2609}
2610
2611pub fn write_protocol_v2_ls_refs_response(
2612    writer: &mut impl Write,
2613    records: &[ProtocolV2LsRefsRecord],
2614) -> Result<()> {
2615    for record in records {
2616        write_pkt_line_payload(
2617            writer,
2618            &line_from_str(&format_protocol_v2_ls_refs_record(record)?),
2619        )?;
2620    }
2621    writer.write_all(b"0000")?;
2622    Ok(())
2623}
2624
2625pub fn read_protocol_v2_ls_refs_response_until_response_end(
2626    format: ObjectFormat,
2627    reader: &mut impl Read,
2628) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2629    let frames = read_pkt_line_frames_until_response_end(reader)?;
2630    parse_protocol_v2_ls_refs_response(format, &frames)
2631}
2632
2633pub fn write_protocol_v2_ls_refs_response_with_response_end(
2634    writer: &mut impl Write,
2635    records: &[ProtocolV2LsRefsRecord],
2636) -> Result<()> {
2637    write_protocol_v2_ls_refs_response(writer, records)?;
2638    writer.write_all(b"0002")?;
2639    Ok(())
2640}
2641
2642pub fn exchange_protocol_v2_ls_refs(
2643    format: ObjectFormat,
2644    reader: &mut impl Read,
2645    writer: &mut impl Write,
2646    request: &ProtocolV2LsRefsRequest,
2647) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2648    write_protocol_v2_ls_refs_request(writer, request)?;
2649    writer.flush()?;
2650    read_protocol_v2_ls_refs_response(format, reader)
2651}
2652
2653/// Bridge a parsed protocol v2 `ls-refs` response into the shared
2654/// [`RefAdvertisementSet`]/[`RefAdvertisement`] types used by the v0/v1 codecs,
2655/// so callers can drive v2 clone/fetch through the same ref-advertisement
2656/// machinery.
2657///
2658/// Each [`ProtocolV2LsRefsRecord::Ref`] becomes a [`RefAdvertisement`]. A
2659/// `peeled:<oid>` attribute is emitted as an additional `<peeled-oid>
2660/// <name>^{}` advertisement, matching the v0/v1 peeled-tag convention.
2661/// `symref-target:<target>` attributes are collected as `symref=<name>:<target>`
2662/// capabilities on the first advertised ref, mirroring how the upload-pack v0/v1
2663/// advertisement carries symrefs. [`ProtocolV2LsRefsRecord::Unborn`] records have
2664/// no object id, so they cannot be represented as a [`RefAdvertisement`]; an
2665/// unborn record carrying a `symref-target` is preserved as a `symref` capability
2666/// while otherwise being skipped. The returned set always reports
2667/// [`ProtocolVersion::V2`].
2668pub fn protocol_v2_ls_refs_records_to_ref_advertisement_set(
2669    records: &[ProtocolV2LsRefsRecord],
2670) -> Result<RefAdvertisementSet> {
2671    let mut refs: Vec<RefAdvertisement> = Vec::new();
2672    let mut symrefs: Vec<Capability> = Vec::new();
2673    for record in records {
2674        match record {
2675            ProtocolV2LsRefsRecord::Ref(reference) => {
2676                validate_protocol_v2_token("ls-refs ref name", &reference.name)?;
2677                refs.push(RefAdvertisement {
2678                    oid: reference.oid,
2679                    name: reference.name.clone(),
2680                    capabilities: Vec::new(),
2681                });
2682                if let Some(peeled) = &reference.peeled {
2683                    refs.push(RefAdvertisement {
2684                        oid: peeled.clone(),
2685                        name: format!("{}^{{}}", reference.name),
2686                        capabilities: Vec::new(),
2687                    });
2688                }
2689                if let Some(target) = &reference.symref_target {
2690                    symrefs.push(protocol_v2_symref_capability(&reference.name, target)?);
2691                }
2692            }
2693            ProtocolV2LsRefsRecord::Unborn {
2694                name,
2695                symref_target,
2696                ..
2697            } => {
2698                validate_protocol_v2_token("ls-refs ref name", name)?;
2699                if let Some(target) = symref_target {
2700                    symrefs.push(protocol_v2_symref_capability(name, target)?);
2701                }
2702            }
2703        }
2704    }
2705    if !symrefs.is_empty() {
2706        if let Some(first) = refs.first_mut() {
2707            first.capabilities = symrefs;
2708        } else {
2709            return Err(GitError::InvalidFormat(
2710                "ls-refs response advertised symrefs without any concrete refs".into(),
2711            ));
2712        }
2713    }
2714    Ok(RefAdvertisementSet {
2715        protocol: ProtocolVersion::V2,
2716        refs,
2717        shallow: Vec::new(),
2718    })
2719}
2720
2721/// Parse a protocol v2 `ls-refs` response and bridge it into the shared
2722/// [`RefAdvertisementSet`] type. Convenience wrapper combining
2723/// [`parse_protocol_v2_ls_refs_response`] and
2724/// [`protocol_v2_ls_refs_records_to_ref_advertisement_set`].
2725pub fn parse_protocol_v2_ls_refs_response_as_ref_advertisement_set(
2726    format: ObjectFormat,
2727    frames: &[PktLineFrame],
2728) -> Result<RefAdvertisementSet> {
2729    let records = parse_protocol_v2_ls_refs_response(format, frames)?;
2730    protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
2731}
2732
2733/// Read a protocol v2 `ls-refs` response from `reader` and bridge it into the
2734/// shared [`RefAdvertisementSet`] type.
2735pub fn read_protocol_v2_ls_refs_response_as_ref_advertisement_set(
2736    format: ObjectFormat,
2737    reader: &mut impl Read,
2738) -> Result<RefAdvertisementSet> {
2739    let records = read_protocol_v2_ls_refs_response(format, reader)?;
2740    protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
2741}
2742
2743fn protocol_v2_symref_capability(name: &str, target: &str) -> Result<Capability> {
2744    validate_protocol_v2_token("ls-refs symref-target", target)?;
2745    Ok(Capability {
2746        name: "symref".into(),
2747        value: Some(format!("{name}:{target}")),
2748    })
2749}
2750
2751pub fn read_protocol_v2_fetch_request(
2752    format: ObjectFormat,
2753    reader: &mut impl Read,
2754) -> Result<ProtocolV2FetchRequest> {
2755    let request = read_protocol_v2_command_request(reader)?;
2756    ProtocolV2FetchRequest::from_command_request(format, &request)
2757}
2758
2759pub fn write_protocol_v2_fetch_request(
2760    writer: &mut impl Write,
2761    request: &ProtocolV2FetchRequest,
2762) -> Result<()> {
2763    let command = request.to_command_request()?;
2764    write_protocol_v2_command_request(writer, &command)
2765}
2766
2767pub fn read_protocol_v2_object_info_request(
2768    format: ObjectFormat,
2769    reader: &mut impl Read,
2770) -> Result<ProtocolV2ObjectInfoRequest> {
2771    let request = read_protocol_v2_command_request(reader)?;
2772    ProtocolV2ObjectInfoRequest::from_command_request(format, &request)
2773}
2774
2775pub fn write_protocol_v2_object_info_request(
2776    writer: &mut impl Write,
2777    request: &ProtocolV2ObjectInfoRequest,
2778) -> Result<()> {
2779    let command = request.to_command_request()?;
2780    write_protocol_v2_command_request(writer, &command)
2781}
2782
2783pub fn parse_protocol_v2_fetch_response(
2784    format: ObjectFormat,
2785    frames: &[PktLineFrame],
2786) -> Result<Vec<ProtocolV2FetchResponseSection>> {
2787    let mut sections = Vec::new();
2788    let mut current: Option<(String, Vec<Vec<u8>>)> = None;
2789    let mut saw_flush = false;
2790    for (idx, frame) in frames.iter().enumerate() {
2791        match frame {
2792            PktLineFrame::Data(payload) => {
2793                if saw_flush {
2794                    return Err(GitError::InvalidFormat(
2795                        "fetch response has data after flush".into(),
2796                    ));
2797                }
2798                if let Some((_name, lines)) = &mut current {
2799                    lines.push(payload.clone());
2800                } else {
2801                    let name = parse_fetch_section_header(payload)?;
2802                    current = Some((name, Vec::new()));
2803                }
2804            }
2805            PktLineFrame::Delimiter => {
2806                if saw_flush {
2807                    return Err(GitError::InvalidFormat(
2808                        "fetch response has delimiter after flush".into(),
2809                    ));
2810                }
2811                let Some((name, lines)) = current.take() else {
2812                    return Err(GitError::InvalidFormat(
2813                        "fetch response has delimiter before section".into(),
2814                    ));
2815                };
2816                sections.push(parse_fetch_section(format, name, lines)?);
2817            }
2818            PktLineFrame::Flush => {
2819                saw_flush = true;
2820                if !flush_terminates_protocol_v2_response(frames, idx) {
2821                    return Err(GitError::InvalidFormat(
2822                        "fetch response has frames after flush".into(),
2823                    ));
2824                }
2825                if let Some((name, lines)) = current.take() {
2826                    sections.push(parse_fetch_section(format, name, lines)?);
2827                }
2828            }
2829            PktLineFrame::ResponseEnd if saw_flush && idx + 1 == frames.len() => {}
2830            PktLineFrame::ResponseEnd => {
2831                return Err(GitError::InvalidFormat(
2832                    "fetch response contains response-end".into(),
2833                ));
2834            }
2835        }
2836    }
2837    if !saw_flush {
2838        return Err(GitError::InvalidFormat(
2839            "fetch response missing flush".into(),
2840        ));
2841    }
2842    Ok(sections)
2843}
2844
2845pub fn encode_protocol_v2_fetch_response(
2846    sections: &[ProtocolV2FetchResponseSection],
2847) -> Result<Vec<PktLineFrame>> {
2848    let mut frames = Vec::new();
2849    for (idx, section) in sections.iter().enumerate() {
2850        if idx != 0 {
2851            frames.push(PktLineFrame::Delimiter);
2852        }
2853        frames.push(PktLineFrame::data(line_from_str(
2854            protocol_v2_fetch_section_name(section),
2855        ))?);
2856        for line in format_protocol_v2_fetch_section_lines(section)? {
2857            frames.push(PktLineFrame::data(line)?);
2858        }
2859    }
2860    frames.push(PktLineFrame::Flush);
2861    Ok(frames)
2862}
2863
2864pub fn parse_protocol_v2_fetch_sideband_all_response(
2865    format: ObjectFormat,
2866    frames: &[PktLineFrame],
2867) -> Result<ProtocolV2FetchSidebandAllResponse> {
2868    let mut demuxed = Vec::new();
2869    let mut progress = Vec::new();
2870    let mut in_packfile = false;
2871    for frame in frames {
2872        match frame {
2873            PktLineFrame::Data(payload) if in_packfile => {
2874                demuxed.push(PktLineFrame::Data(payload.clone()));
2875            }
2876            PktLineFrame::Data(payload) => {
2877                let packet = parse_sideband_packet(payload)?;
2878                match packet.channel {
2879                    SideBandChannel::Data => {
2880                        if trim_trailing_lf(&packet.data) == b"packfile" {
2881                            in_packfile = true;
2882                        }
2883                        demuxed.push(PktLineFrame::Data(packet.data));
2884                    }
2885                    SideBandChannel::Progress => progress.push(packet.data),
2886                    SideBandChannel::Fatal => {
2887                        let message = String::from_utf8_lossy(&packet.data).into_owned();
2888                        return Err(GitError::InvalidFormat(format!(
2889                            "sideband fatal: {message}"
2890                        )));
2891                    }
2892                }
2893            }
2894            PktLineFrame::Delimiter => {
2895                in_packfile = false;
2896                demuxed.push(PktLineFrame::Delimiter);
2897            }
2898            PktLineFrame::Flush => {
2899                in_packfile = false;
2900                demuxed.push(PktLineFrame::Flush);
2901            }
2902            PktLineFrame::ResponseEnd => {
2903                in_packfile = false;
2904                demuxed.push(PktLineFrame::ResponseEnd);
2905            }
2906        }
2907    }
2908    Ok(ProtocolV2FetchSidebandAllResponse {
2909        sections: parse_protocol_v2_fetch_response(format, &demuxed)?,
2910        progress,
2911    })
2912}
2913
2914pub fn encode_protocol_v2_fetch_sideband_all_response(
2915    sections: &[ProtocolV2FetchResponseSection],
2916) -> Result<Vec<PktLineFrame>> {
2917    let frames = encode_protocol_v2_fetch_response(sections)?;
2918    let mut encoded = Vec::new();
2919    let mut in_packfile = false;
2920    for frame in frames {
2921        match frame {
2922            PktLineFrame::Data(payload) if in_packfile => {
2923                encoded.push(PktLineFrame::Data(payload));
2924            }
2925            PktLineFrame::Data(payload) => {
2926                if trim_trailing_lf(&payload) == b"packfile" {
2927                    in_packfile = true;
2928                }
2929                encoded.push(PktLineFrame::data(encode_sideband_packet(
2930                    &SideBandPacket {
2931                        channel: SideBandChannel::Data,
2932                        data: payload,
2933                    },
2934                )?)?);
2935            }
2936            PktLineFrame::Delimiter => {
2937                in_packfile = false;
2938                encoded.push(PktLineFrame::Delimiter);
2939            }
2940            PktLineFrame::Flush => {
2941                in_packfile = false;
2942                encoded.push(PktLineFrame::Flush);
2943            }
2944            PktLineFrame::ResponseEnd => {
2945                in_packfile = false;
2946                encoded.push(PktLineFrame::ResponseEnd);
2947            }
2948        }
2949    }
2950    Ok(encoded)
2951}
2952
2953pub fn read_protocol_v2_fetch_response(
2954    format: ObjectFormat,
2955    reader: &mut impl Read,
2956) -> Result<Vec<ProtocolV2FetchResponseSection>> {
2957    let frames = read_pkt_line_frames_until_flush(reader)?;
2958    parse_protocol_v2_fetch_response(format, &frames)
2959}
2960
2961pub fn read_protocol_v2_fetch_response_header(
2962    format: ObjectFormat,
2963    reader: &mut impl Read,
2964    sideband_all: bool,
2965) -> Result<ProtocolV2FetchResponseHeader> {
2966    let mut sections = Vec::new();
2967    let mut current: Option<(String, Vec<Vec<u8>>)> = None;
2968    loop {
2969        let frame = read_protocol_v2_fetch_metadata_frame(reader, sideband_all)?;
2970        match frame {
2971            PktLineFrame::Data(payload) => {
2972                if let Some((_name, lines)) = &mut current {
2973                    lines.push(payload);
2974                } else {
2975                    let name = parse_fetch_section_header(&payload)?;
2976                    if name == "packfile" {
2977                        return Ok(ProtocolV2FetchResponseHeader {
2978                            sections,
2979                            has_packfile: true,
2980                        });
2981                    }
2982                    current = Some((name, Vec::new()));
2983                }
2984            }
2985            PktLineFrame::Delimiter => {
2986                let Some((name, lines)) = current.take() else {
2987                    return Err(GitError::InvalidFormat(
2988                        "fetch response has delimiter before section".into(),
2989                    ));
2990                };
2991                sections.push(parse_fetch_section(format, name, lines)?);
2992            }
2993            PktLineFrame::Flush => {
2994                if let Some((name, lines)) = current.take() {
2995                    sections.push(parse_fetch_section(format, name, lines)?);
2996                }
2997                return Ok(ProtocolV2FetchResponseHeader {
2998                    sections,
2999                    has_packfile: false,
3000                });
3001            }
3002            PktLineFrame::ResponseEnd => {
3003                return Err(GitError::InvalidFormat(
3004                    "fetch response contains response-end".into(),
3005                ));
3006            }
3007        }
3008    }
3009}
3010
3011fn read_protocol_v2_fetch_metadata_frame(
3012    reader: &mut impl Read,
3013    sideband_all: bool,
3014) -> Result<PktLineFrame> {
3015    loop {
3016        let frame = read_pkt_line_frame(reader)?
3017            .ok_or_else(|| GitError::InvalidFormat("fetch response ended before flush".into()))?;
3018        if sideband_all && let PktLineFrame::Data(payload) = frame {
3019            let packet = parse_sideband_packet(&payload)?;
3020            match packet.channel {
3021                SideBandChannel::Data => return Ok(PktLineFrame::Data(packet.data)),
3022                SideBandChannel::Progress => continue,
3023                SideBandChannel::Fatal => {
3024                    let message = String::from_utf8_lossy(&packet.data).into_owned();
3025                    return Err(GitError::InvalidFormat(format!(
3026                        "sideband fatal: {message}"
3027                    )));
3028                }
3029            }
3030        }
3031        return Ok(frame);
3032    }
3033}
3034
3035pub fn write_protocol_v2_fetch_response(
3036    writer: &mut impl Write,
3037    sections: &[ProtocolV2FetchResponseSection],
3038) -> Result<()> {
3039    write_protocol_v2_fetch_response_inner(writer, sections, false, false)
3040}
3041
3042pub fn read_protocol_v2_fetch_sideband_all_response(
3043    format: ObjectFormat,
3044    reader: &mut impl Read,
3045) -> Result<ProtocolV2FetchSidebandAllResponse> {
3046    let frames = read_pkt_line_frames_until_flush(reader)?;
3047    parse_protocol_v2_fetch_sideband_all_response(format, &frames)
3048}
3049
3050pub fn write_protocol_v2_fetch_sideband_all_response(
3051    writer: &mut impl Write,
3052    sections: &[ProtocolV2FetchResponseSection],
3053) -> Result<()> {
3054    write_protocol_v2_fetch_response_inner(writer, sections, true, false)
3055}
3056
3057pub fn read_protocol_v2_fetch_response_until_response_end(
3058    format: ObjectFormat,
3059    reader: &mut impl Read,
3060) -> Result<Vec<ProtocolV2FetchResponseSection>> {
3061    let frames = read_pkt_line_frames_until_response_end(reader)?;
3062    parse_protocol_v2_fetch_response(format, &frames)
3063}
3064
3065pub fn write_protocol_v2_fetch_response_with_response_end(
3066    writer: &mut impl Write,
3067    sections: &[ProtocolV2FetchResponseSection],
3068) -> Result<()> {
3069    write_protocol_v2_fetch_response_inner(writer, sections, false, true)
3070}
3071
3072pub fn read_protocol_v2_fetch_sideband_all_response_until_response_end(
3073    format: ObjectFormat,
3074    reader: &mut impl Read,
3075) -> Result<ProtocolV2FetchSidebandAllResponse> {
3076    let frames = read_pkt_line_frames_until_response_end(reader)?;
3077    parse_protocol_v2_fetch_sideband_all_response(format, &frames)
3078}
3079
3080pub fn write_protocol_v2_fetch_sideband_all_response_with_response_end(
3081    writer: &mut impl Write,
3082    sections: &[ProtocolV2FetchResponseSection],
3083) -> Result<()> {
3084    write_protocol_v2_fetch_response_inner(writer, sections, true, true)
3085}
3086
3087fn write_protocol_v2_fetch_response_inner(
3088    writer: &mut impl Write,
3089    sections: &[ProtocolV2FetchResponseSection],
3090    sideband_all: bool,
3091    response_end: bool,
3092) -> Result<()> {
3093    let mut in_packfile = false;
3094    for (idx, section) in sections.iter().enumerate() {
3095        if idx != 0 {
3096            in_packfile = false;
3097            write_pkt_line_frame(writer, &PktLineFrame::Delimiter)?;
3098        }
3099        write_protocol_v2_fetch_payload(
3100            writer,
3101            &line_from_str(protocol_v2_fetch_section_name(section)),
3102            sideband_all,
3103            &mut in_packfile,
3104        )?;
3105        for payload in format_protocol_v2_fetch_section_lines(section)? {
3106            write_protocol_v2_fetch_payload(writer, &payload, sideband_all, &mut in_packfile)?;
3107        }
3108    }
3109    writer.write_all(b"0000")?;
3110    if response_end {
3111        writer.write_all(b"0002")?;
3112    }
3113    Ok(())
3114}
3115
3116fn write_protocol_v2_fetch_payload(
3117    writer: &mut impl Write,
3118    payload: &[u8],
3119    sideband_all: bool,
3120    in_packfile: &mut bool,
3121) -> Result<()> {
3122    if sideband_all && !*in_packfile {
3123        if trim_trailing_lf(payload) == b"packfile" {
3124            *in_packfile = true;
3125        }
3126        write_sideband_payload(writer, SideBandChannel::Data, payload)
3127    } else {
3128        write_pkt_line_payload(writer, payload)
3129    }
3130}
3131
3132pub fn exchange_protocol_v2_fetch(
3133    format: ObjectFormat,
3134    reader: &mut impl Read,
3135    writer: &mut impl Write,
3136    request: &ProtocolV2FetchRequest,
3137) -> Result<Vec<ProtocolV2FetchResponseSection>> {
3138    write_protocol_v2_fetch_request(writer, request)?;
3139    writer.flush()?;
3140    read_protocol_v2_fetch_response(format, reader)
3141}
3142
3143pub fn parse_protocol_v2_object_info_response(
3144    format: ObjectFormat,
3145    frames: &[PktLineFrame],
3146) -> Result<ProtocolV2ObjectInfoResponse> {
3147    let Some((first, rest)) = frames.split_first() else {
3148        return Err(GitError::InvalidFormat(
3149            "object-info response is empty".into(),
3150        ));
3151    };
3152    let PktLineFrame::Data(attrs) = first else {
3153        return Err(GitError::InvalidFormat(
3154            "object-info response must start with attributes".into(),
3155        ));
3156    };
3157    let attrs = parse_protocol_v2_line_text("object-info response attributes", attrs)?;
3158    let mut response = ProtocolV2ObjectInfoResponse::default();
3159    for attr in attrs.split(' ') {
3160        validate_protocol_v2_token("object-info response attribute", attr)?;
3161        match attr {
3162            "size" => {
3163                if response.size {
3164                    return Err(GitError::InvalidFormat(
3165                        "object-info response has duplicate size attribute".into(),
3166                    ));
3167                }
3168                response.size = true;
3169            }
3170            other => {
3171                return Err(GitError::InvalidFormat(format!(
3172                    "unsupported object-info response attribute {other}"
3173                )));
3174            }
3175        }
3176    }
3177    if !response.size {
3178        return Err(GitError::InvalidFormat(
3179            "object-info response is missing size attribute".into(),
3180        ));
3181    }
3182
3183    let mut saw_flush = false;
3184    for (idx, frame) in rest.iter().enumerate() {
3185        match frame {
3186            PktLineFrame::Data(payload) if !saw_flush => {
3187                response
3188                    .records
3189                    .push(parse_protocol_v2_object_info_record(format, payload)?);
3190            }
3191            PktLineFrame::Data(_) => {
3192                return Err(GitError::InvalidFormat(
3193                    "object-info response has data after flush".into(),
3194                ));
3195            }
3196            PktLineFrame::Flush => {
3197                saw_flush = true;
3198                if idx + 1 != rest.len() {
3199                    return Err(GitError::InvalidFormat(
3200                        "object-info response has frames after flush".into(),
3201                    ));
3202                }
3203            }
3204            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
3205                return Err(GitError::InvalidFormat(
3206                    "object-info response contains a non-flush control packet".into(),
3207                ));
3208            }
3209        }
3210    }
3211    if !saw_flush {
3212        return Err(GitError::InvalidFormat(
3213            "object-info response missing flush".into(),
3214        ));
3215    }
3216    Ok(response)
3217}
3218
3219pub fn encode_protocol_v2_object_info_response(
3220    response: &ProtocolV2ObjectInfoResponse,
3221) -> Result<Vec<PktLineFrame>> {
3222    if !response.size {
3223        return Err(GitError::InvalidFormat(
3224            "object-info response is missing size attribute".into(),
3225        ));
3226    }
3227    let mut frames = Vec::new();
3228    frames.push(PktLineFrame::data(line_from_str("size"))?);
3229    for record in &response.records {
3230        frames.push(PktLineFrame::data(line_from_str(&format!(
3231            "{} {}",
3232            record.oid, record.size
3233        )))?);
3234    }
3235    frames.push(PktLineFrame::Flush);
3236    Ok(frames)
3237}
3238
3239pub fn read_protocol_v2_object_info_response(
3240    format: ObjectFormat,
3241    reader: &mut impl Read,
3242) -> Result<ProtocolV2ObjectInfoResponse> {
3243    let frames = read_pkt_line_frames_until_flush(reader)?;
3244    parse_protocol_v2_object_info_response(format, &frames)
3245}
3246
3247pub fn write_protocol_v2_object_info_response(
3248    writer: &mut impl Write,
3249    response: &ProtocolV2ObjectInfoResponse,
3250) -> Result<()> {
3251    if !response.size {
3252        return Err(GitError::InvalidFormat(
3253            "object-info response is missing size attribute".into(),
3254        ));
3255    }
3256    write_pkt_line_payload(writer, b"size\n")?;
3257    for record in &response.records {
3258        write_pkt_line_payload(
3259            writer,
3260            &line_from_str(&format!("{} {}", record.oid, record.size)),
3261        )?;
3262    }
3263    writer.write_all(b"0000")?;
3264    Ok(())
3265}
3266
3267pub fn exchange_protocol_v2_object_info(
3268    format: ObjectFormat,
3269    reader: &mut impl Read,
3270    writer: &mut impl Write,
3271    request: &ProtocolV2ObjectInfoRequest,
3272) -> Result<ProtocolV2ObjectInfoResponse> {
3273    write_protocol_v2_object_info_request(writer, request)?;
3274    writer.flush()?;
3275    read_protocol_v2_object_info_response(format, reader)
3276}
3277
3278pub fn demux_protocol_v2_fetch_packfile(
3279    sections: &[ProtocolV2FetchResponseSection],
3280) -> Result<Option<SideBandDemux>> {
3281    let mut packfile = None;
3282    for section in sections {
3283        if let ProtocolV2FetchResponseSection::Packfile(lines) = section {
3284            if packfile.is_some() {
3285                return Err(GitError::InvalidFormat(
3286                    "fetch response has duplicate packfile sections".into(),
3287                ));
3288            }
3289            packfile = Some(parse_and_demux_sideband_packets(lines)?);
3290        }
3291    }
3292    Ok(packfile)
3293}
3294
3295pub fn protocol_v2_object_format(capabilities: &[Capability]) -> Result<ObjectFormat> {
3296    let mut format = None;
3297    for capability in capabilities {
3298        if capability.name != "object-format" {
3299            continue;
3300        }
3301        if format.is_some() {
3302            return Err(GitError::InvalidFormat(
3303                "protocol v2 has duplicate object-format capabilities".into(),
3304            ));
3305        }
3306        let Some(value) = &capability.value else {
3307            return Err(GitError::InvalidFormat(
3308                "protocol v2 object-format capability is missing a value".into(),
3309            ));
3310        };
3311        format = Some(value.parse::<ObjectFormat>()?);
3312    }
3313    Ok(format.unwrap_or(ObjectFormat::Sha1))
3314}
3315
3316pub fn validate_protocol_v2_command_request_capabilities(
3317    handshake: &TransportHandshake,
3318    request: &ProtocolV2CommandRequest,
3319) -> Result<()> {
3320    if handshake.protocol != ProtocolVersion::V2 {
3321        return Err(GitError::InvalidFormat(
3322            "protocol v2 command validation requires a v2 handshake".into(),
3323        ));
3324    }
3325    let advertised =
3326        protocol_v2_capability(&handshake.capabilities, &request.command).ok_or_else(|| {
3327            GitError::InvalidFormat(format!("unadvertised command {}", request.command))
3328        })?;
3329    if advertised.name.is_empty() {
3330        return Err(GitError::InvalidFormat(
3331            "advertised command capability is empty".into(),
3332        ));
3333    }
3334    parse_protocol_v2_command_options(&request.capabilities)?;
3335
3336    for capability in &request.capabilities {
3337        let advertised = protocol_v2_capability(&handshake.capabilities, &capability.name)
3338            .ok_or_else(|| {
3339                GitError::InvalidFormat(format!(
3340                    "unadvertised protocol v2 capability {}",
3341                    capability.name
3342                ))
3343            })?;
3344        if capability.name == "object-format" {
3345            validate_protocol_v2_object_format_request(advertised, capability)?;
3346        }
3347    }
3348    Ok(())
3349}
3350
3351pub fn parse_protocol_v2_command_options(
3352    capabilities: &[Capability],
3353) -> Result<ProtocolV2CommandOptions> {
3354    let mut out = ProtocolV2CommandOptions::default();
3355    for capability in capabilities {
3356        match capability.name.as_str() {
3357            "agent" => {
3358                if out.agent.is_some() {
3359                    return Err(GitError::InvalidFormat(
3360                        "protocol v2 command has duplicate agent capabilities".into(),
3361                    ));
3362                }
3363                let Some(value) = &capability.value else {
3364                    return Err(GitError::InvalidFormat(
3365                        "protocol v2 agent capability is missing a value".into(),
3366                    ));
3367                };
3368                validate_protocol_v2_capability_value(value)?;
3369                out.agent = Some(value.clone());
3370            }
3371            "object-format" => {
3372                if out.object_format.is_some() {
3373                    return Err(GitError::InvalidFormat(
3374                        "protocol v2 command has duplicate object-format capabilities".into(),
3375                    ));
3376                }
3377                let Some(value) = &capability.value else {
3378                    return Err(GitError::InvalidFormat(
3379                        "protocol v2 object-format capability is missing a value".into(),
3380                    ));
3381                };
3382                out.object_format = Some(value.parse::<ObjectFormat>()?);
3383            }
3384            "server-option" => {
3385                let Some(value) = &capability.value else {
3386                    return Err(GitError::InvalidFormat(
3387                        "protocol v2 server-option capability is missing a value".into(),
3388                    ));
3389                };
3390                validate_protocol_v2_capability_value(value)?;
3391                out.server_options.push(value.clone());
3392            }
3393            _ => out.extra.push(capability.clone()),
3394        }
3395    }
3396    Ok(out)
3397}
3398
3399pub fn encode_protocol_v2_command_options(
3400    options: &ProtocolV2CommandOptions,
3401) -> Result<Vec<Capability>> {
3402    let mut capabilities = Vec::new();
3403    if let Some(agent) = &options.agent {
3404        validate_protocol_v2_capability_value(agent)?;
3405        capabilities.push(Capability {
3406            name: "agent".into(),
3407            value: Some(agent.clone()),
3408        });
3409    }
3410    if let Some(format) = options.object_format {
3411        capabilities.push(Capability {
3412            name: "object-format".into(),
3413            value: Some(format.name().into()),
3414        });
3415    }
3416    for option in &options.server_options {
3417        validate_protocol_v2_capability_value(option)?;
3418        capabilities.push(Capability {
3419            name: "server-option".into(),
3420            value: Some(option.clone()),
3421        });
3422    }
3423    for capability in &options.extra {
3424        if matches!(
3425            capability.name.as_str(),
3426            "agent" | "object-format" | "server-option"
3427        ) {
3428            return Err(GitError::InvalidFormat(format!(
3429                "protocol v2 extra capability duplicates known capability {}",
3430                capability.name
3431            )));
3432        }
3433        encode_protocol_v2_capability(capability)?;
3434        capabilities.push(capability.clone());
3435    }
3436    Ok(capabilities)
3437}
3438
3439pub fn parse_protocol_v2_ls_refs_features(
3440    capabilities: &[Capability],
3441) -> Result<Option<ProtocolV2LsRefsFeatures>> {
3442    let mut ls_refs = None;
3443    for capability in capabilities {
3444        if capability.name != "ls-refs" {
3445            continue;
3446        }
3447        if ls_refs.is_some() {
3448            return Err(GitError::InvalidFormat(
3449                "protocol v2 has duplicate ls-refs capabilities".into(),
3450            ));
3451        }
3452        ls_refs = Some(parse_protocol_v2_ls_refs_feature_value(
3453            capability.value.as_deref(),
3454        )?);
3455    }
3456    Ok(ls_refs)
3457}
3458
3459pub fn encode_protocol_v2_ls_refs_capability(
3460    features: &ProtocolV2LsRefsFeatures,
3461) -> Result<Capability> {
3462    let mut values = Vec::new();
3463    if features.unborn {
3464        values.push("unborn".to_string());
3465    }
3466    for feature in &features.unknown {
3467        validate_protocol_v2_token("ls-refs feature", feature)?;
3468        if feature == "unborn" {
3469            return Err(GitError::InvalidFormat(
3470                "ls-refs unknown features must not duplicate known feature unborn".into(),
3471            ));
3472        }
3473        values.push(feature.clone());
3474    }
3475    Ok(Capability {
3476        name: "ls-refs".into(),
3477        value: (!values.is_empty()).then(|| values.join(" ")),
3478    })
3479}
3480
3481pub fn validate_protocol_v2_ls_refs_request_features(
3482    features: &ProtocolV2LsRefsFeatures,
3483    request: &ProtocolV2LsRefsRequest,
3484) -> Result<()> {
3485    if request.unborn && !features.unborn {
3486        return Err(GitError::InvalidFormat(
3487            "ls-refs request uses unborn without advertised unborn feature".into(),
3488        ));
3489    }
3490    Ok(())
3491}
3492
3493pub fn validate_protocol_v2_ls_refs_command_request(
3494    handshake: &TransportHandshake,
3495    request: &ProtocolV2CommandRequest,
3496) -> Result<ProtocolV2LsRefsRequest> {
3497    validate_protocol_v2_command_request_capabilities(handshake, request)?;
3498    let ls_refs = ProtocolV2LsRefsRequest::from_command_request(request)?;
3499    let features = parse_protocol_v2_ls_refs_features(&handshake.capabilities)?
3500        .ok_or_else(|| GitError::InvalidFormat("ls-refs command was not advertised".into()))?;
3501    validate_protocol_v2_ls_refs_request_features(&features, &ls_refs)?;
3502    Ok(ls_refs)
3503}
3504
3505pub fn parse_protocol_v2_fetch_features(
3506    capabilities: &[Capability],
3507) -> Result<Option<ProtocolV2FetchFeatures>> {
3508    let mut fetch = None;
3509    for capability in capabilities {
3510        if capability.name != "fetch" {
3511            continue;
3512        }
3513        if fetch.is_some() {
3514            return Err(GitError::InvalidFormat(
3515                "protocol v2 has duplicate fetch capabilities".into(),
3516            ));
3517        }
3518        fetch = Some(parse_protocol_v2_fetch_feature_value(
3519            capability.value.as_deref(),
3520        )?);
3521    }
3522    Ok(fetch)
3523}
3524
3525pub fn encode_protocol_v2_fetch_capability(
3526    features: &ProtocolV2FetchFeatures,
3527) -> Result<Capability> {
3528    let mut values = Vec::new();
3529    if features.shallow {
3530        values.push("shallow".to_string());
3531    }
3532    if features.wait_for_done {
3533        values.push("wait-for-done".to_string());
3534    }
3535    if features.filter {
3536        values.push("filter".to_string());
3537    }
3538    if features.ref_in_want {
3539        values.push("ref-in-want".to_string());
3540    }
3541    if features.sideband_all {
3542        values.push("sideband-all".to_string());
3543    }
3544    if features.packfile_uris {
3545        values.push("packfile-uris".to_string());
3546    }
3547    for feature in &features.unknown {
3548        validate_protocol_v2_token("fetch feature", feature)?;
3549        if matches!(
3550            feature.as_str(),
3551            "shallow"
3552                | "wait-for-done"
3553                | "filter"
3554                | "ref-in-want"
3555                | "sideband-all"
3556                | "packfile-uris"
3557        ) {
3558            return Err(GitError::InvalidFormat(format!(
3559                "fetch unknown features must not duplicate known feature {feature}"
3560            )));
3561        }
3562        values.push(feature.clone());
3563    }
3564    Ok(Capability {
3565        name: "fetch".into(),
3566        value: (!values.is_empty()).then(|| values.join(" ")),
3567    })
3568}
3569
3570pub fn validate_protocol_v2_fetch_request_features(
3571    features: &ProtocolV2FetchFeatures,
3572    request: &ProtocolV2FetchRequest,
3573) -> Result<()> {
3574    if !features.shallow
3575        && (!request.shallow.is_empty()
3576            || request.deepen.is_some()
3577            || request.deepen_since.is_some()
3578            || !request.deepen_not.is_empty()
3579            || request.deepen_relative)
3580    {
3581        return Err(GitError::InvalidFormat(
3582            "fetch request uses shallow/deepen arguments without advertised shallow feature".into(),
3583        ));
3584    }
3585    if !features.filter && request.filter.is_some() {
3586        return Err(GitError::InvalidFormat(
3587            "fetch request uses filter without advertised filter feature".into(),
3588        ));
3589    }
3590    if !features.ref_in_want && !request.want_refs.is_empty() {
3591        return Err(GitError::InvalidFormat(
3592            "fetch request uses want-ref without advertised ref-in-want feature".into(),
3593        ));
3594    }
3595    if !features.sideband_all && request.sideband_all {
3596        return Err(GitError::InvalidFormat(
3597            "fetch request uses sideband-all without advertised sideband-all feature".into(),
3598        ));
3599    }
3600    if !features.packfile_uris && request.packfile_uris.is_some() {
3601        return Err(GitError::InvalidFormat(
3602            "fetch request uses packfile-uris without advertised packfile-uris feature".into(),
3603        ));
3604    }
3605    if !features.wait_for_done && request.wait_for_done {
3606        return Err(GitError::InvalidFormat(
3607            "fetch request uses wait-for-done without advertised wait-for-done feature".into(),
3608        ));
3609    }
3610    Ok(())
3611}
3612
3613pub fn validate_protocol_v2_fetch_command_request(
3614    handshake: &TransportHandshake,
3615    format: ObjectFormat,
3616    request: &ProtocolV2CommandRequest,
3617) -> Result<ProtocolV2FetchRequest> {
3618    validate_protocol_v2_command_request_capabilities(handshake, request)?;
3619    let fetch = ProtocolV2FetchRequest::from_command_request(format, request)?;
3620    let features = parse_protocol_v2_fetch_features(&handshake.capabilities)?
3621        .ok_or_else(|| GitError::InvalidFormat("fetch command was not advertised".into()))?;
3622    validate_protocol_v2_fetch_request_features(&features, &fetch)?;
3623    Ok(fetch)
3624}
3625
3626pub fn validate_protocol_v2_object_info_command_request(
3627    handshake: &TransportHandshake,
3628    format: ObjectFormat,
3629    request: &ProtocolV2CommandRequest,
3630) -> Result<ProtocolV2ObjectInfoRequest> {
3631    validate_protocol_v2_command_request_capabilities(handshake, request)?;
3632    let object_info = ProtocolV2ObjectInfoRequest::from_command_request(format, request)?;
3633    protocol_v2_capability(&handshake.capabilities, "object-info")
3634        .ok_or_else(|| GitError::InvalidFormat("object-info command was not advertised".into()))?;
3635    Ok(object_info)
3636}
3637
3638pub fn classify_protocol_v2_command_request(
3639    handshake: &TransportHandshake,
3640    format: ObjectFormat,
3641    request: &ProtocolV2CommandRequest,
3642) -> Result<ProtocolV2Command> {
3643    match request.command.as_str() {
3644        "ls-refs" => validate_protocol_v2_ls_refs_command_request(handshake, request)
3645            .map(ProtocolV2Command::LsRefs),
3646        "fetch" => validate_protocol_v2_fetch_command_request(handshake, format, request)
3647            .map(ProtocolV2Command::Fetch),
3648        "object-info" => {
3649            validate_protocol_v2_object_info_command_request(handshake, format, request)
3650                .map(ProtocolV2Command::ObjectInfo)
3651        }
3652        _ => {
3653            validate_protocol_v2_command_request_capabilities(handshake, request)?;
3654            Ok(ProtocolV2Command::Unknown(request.clone()))
3655        }
3656    }
3657}
3658
3659pub fn classify_protocol_v2_request(
3660    handshake: &TransportHandshake,
3661    format: ObjectFormat,
3662    request: &ProtocolV2Request,
3663) -> Result<ProtocolV2SessionRequest> {
3664    match request {
3665        ProtocolV2Request::Command(command) => {
3666            classify_protocol_v2_command_request(handshake, format, command)
3667                .map(ProtocolV2SessionRequest::Command)
3668        }
3669        ProtocolV2Request::Done => Ok(ProtocolV2SessionRequest::Done),
3670    }
3671}
3672
3673pub fn read_protocol_v2_session_request(
3674    handshake: &TransportHandshake,
3675    format: ObjectFormat,
3676    reader: &mut impl Read,
3677) -> Result<ProtocolV2SessionRequest> {
3678    let request = read_protocol_v2_request(reader)?;
3679    classify_protocol_v2_request(handshake, format, &request)
3680}
3681
3682fn protocol_v2_capability<'a>(
3683    capabilities: &'a [Capability],
3684    name: &str,
3685) -> Option<&'a Capability> {
3686    capabilities
3687        .iter()
3688        .find(|capability| capability.name == name)
3689}
3690
3691fn validate_protocol_v2_object_format_request(
3692    advertised: &Capability,
3693    requested: &Capability,
3694) -> Result<()> {
3695    let Some(advertised) = &advertised.value else {
3696        return Err(GitError::InvalidFormat(
3697            "advertised object-format capability is missing a value".into(),
3698        ));
3699    };
3700    let Some(requested) = &requested.value else {
3701        return Err(GitError::InvalidFormat(
3702            "requested object-format capability is missing a value".into(),
3703        ));
3704    };
3705    if advertised != requested {
3706        return Err(GitError::InvalidFormat(format!(
3707            "requested object-format {requested} does not match advertised {advertised}"
3708        )));
3709    }
3710    Ok(())
3711}
3712
3713fn parse_protocol_v2_ls_refs_feature_value(
3714    value: Option<&str>,
3715) -> Result<ProtocolV2LsRefsFeatures> {
3716    let mut out = ProtocolV2LsRefsFeatures::default();
3717    let Some(value) = value else {
3718        return Ok(out);
3719    };
3720    if value.is_empty() {
3721        return Err(GitError::InvalidFormat(
3722            "protocol v2 ls-refs capability value is empty".into(),
3723        ));
3724    }
3725    for feature in value.split(' ') {
3726        validate_protocol_v2_token("ls-refs feature", feature)?;
3727        match feature {
3728            "unborn" => out.unborn = true,
3729            other => out.unknown.push(other.to_string()),
3730        }
3731    }
3732    Ok(out)
3733}
3734
3735fn parse_protocol_v2_fetch_feature_value(value: Option<&str>) -> Result<ProtocolV2FetchFeatures> {
3736    let mut out = ProtocolV2FetchFeatures::default();
3737    let Some(value) = value else {
3738        return Ok(out);
3739    };
3740    if value.is_empty() {
3741        return Err(GitError::InvalidFormat(
3742            "protocol v2 fetch capability value is empty".into(),
3743        ));
3744    }
3745    for feature in value.split(' ') {
3746        validate_protocol_v2_token("fetch feature", feature)?;
3747        match feature {
3748            "shallow" => out.shallow = true,
3749            "wait-for-done" => out.wait_for_done = true,
3750            "filter" => out.filter = true,
3751            "ref-in-want" => out.ref_in_want = true,
3752            "sideband-all" => out.sideband_all = true,
3753            "packfile-uris" => out.packfile_uris = true,
3754            other => out.unknown.push(other.to_string()),
3755        }
3756    }
3757    Ok(out)
3758}
3759
3760pub fn parse_capabilities(input: &[u8]) -> Result<Vec<Capability>> {
3761    let input = trim_trailing_lf(input);
3762    if input.is_empty() {
3763        return Ok(Vec::new());
3764    }
3765    let text =
3766        std::str::from_utf8(input).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
3767    text.split(' ')
3768        .map(parse_capability_token)
3769        .collect::<Result<Vec<_>>>()
3770}
3771
3772pub fn encode_capabilities(capabilities: &[Capability]) -> Result<Vec<u8>> {
3773    let mut out = Vec::new();
3774    for (idx, capability) in capabilities.iter().enumerate() {
3775        validate_capability_field("capability name", &capability.name)?;
3776        if idx != 0 {
3777            out.push(b' ');
3778        }
3779        out.extend_from_slice(capability.name.as_bytes());
3780        if let Some(value) = &capability.value {
3781            validate_capability_field("capability value", value)?;
3782            out.push(b'=');
3783            out.extend_from_slice(value.as_bytes());
3784        }
3785    }
3786    Ok(out)
3787}
3788
3789pub fn parse_ref_advertisement(format: ObjectFormat, payload: &[u8]) -> Result<RefAdvertisement> {
3790    let payload = trim_trailing_lf(payload);
3791    let (reference, capabilities) = match payload.iter().position(|byte| *byte == 0) {
3792        Some(idx) => (&payload[..idx], parse_capabilities(&payload[idx + 1..])?),
3793        None => (payload, Vec::new()),
3794    };
3795    let text =
3796        std::str::from_utf8(reference).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
3797    let (oid, name) = text
3798        .split_once(' ')
3799        .ok_or_else(|| GitError::InvalidFormat("advertised ref is missing name".into()))?;
3800    if name.is_empty() {
3801        return Err(GitError::InvalidFormat(
3802            "advertised ref name is empty".into(),
3803        ));
3804    }
3805    Ok(RefAdvertisement {
3806        oid: ObjectId::from_hex(format, oid)?,
3807        name: name.to_string(),
3808        capabilities,
3809    })
3810}
3811
3812pub fn encode_ref_advertisement(advertisement: &RefAdvertisement) -> Result<Vec<u8>> {
3813    validate_protocol_v2_token("advertised ref name", &advertisement.name)?;
3814    let mut out = advertisement.oid.to_string().into_bytes();
3815    out.push(b' ');
3816    out.extend_from_slice(advertisement.name.as_bytes());
3817    if !advertisement.capabilities.is_empty() {
3818        out.push(0);
3819        out.extend_from_slice(&encode_capabilities(&advertisement.capabilities)?);
3820    }
3821    out.push(b'\n');
3822    Ok(out)
3823}
3824
3825pub fn parse_ref_advertisements(
3826    format: ObjectFormat,
3827    frames: &[PktLineFrame],
3828) -> Result<Vec<RefAdvertisement>> {
3829    Ok(parse_ref_advertisement_set(format, frames)?.refs)
3830}
3831
3832pub fn parse_ref_advertisement_set(
3833    format: ObjectFormat,
3834    frames: &[PktLineFrame],
3835) -> Result<RefAdvertisementSet> {
3836    let mut set = RefAdvertisementSet {
3837        protocol: ProtocolVersion::V0,
3838        refs: Vec::new(),
3839        shallow: Vec::new(),
3840    };
3841    let mut saw_flush = false;
3842    let mut in_shallow = false;
3843    for (idx, frame) in frames.iter().enumerate() {
3844        match frame {
3845            PktLineFrame::Data(payload) if !saw_flush => {
3846                let trimmed = trim_trailing_lf(payload);
3847                if trimmed == b"version 1" {
3848                    if idx != 0 {
3849                        return Err(GitError::InvalidFormat(
3850                            "advertised ref protocol version must be the first line".into(),
3851                        ));
3852                    }
3853                    set.protocol = ProtocolVersion::V1;
3854                    continue;
3855                }
3856                if trimmed.starts_with(b"version ") {
3857                    return Err(GitError::InvalidFormat(
3858                        "unsupported advertised ref protocol version".into(),
3859                    ));
3860                }
3861                if trimmed.starts_with(b"shallow ") {
3862                    if set.refs.is_empty() {
3863                        return Err(GitError::InvalidFormat(
3864                            "advertised shallow refs must follow advertised refs".into(),
3865                        ));
3866                    }
3867                    let text = parse_protocol_v2_line_text("advertised shallow ref", payload)?;
3868                    set.shallow.push(parse_oid_argument(
3869                        format,
3870                        "advertised shallow ref",
3871                        text,
3872                        "shallow ",
3873                    )?);
3874                    in_shallow = true;
3875                    continue;
3876                }
3877                if in_shallow {
3878                    return Err(GitError::InvalidFormat(
3879                        "advertised refs must not follow shallow refs".into(),
3880                    ));
3881                }
3882                let advertisement = parse_ref_advertisement(format, payload)?;
3883                if !set.refs.is_empty() && !advertisement.capabilities.is_empty() {
3884                    return Err(GitError::InvalidFormat(
3885                        "advertised ref capabilities must appear on the first ref".into(),
3886                    ));
3887                }
3888                set.refs.push(advertisement);
3889            }
3890            PktLineFrame::Data(_) => {
3891                return Err(GitError::InvalidFormat(
3892                    "advertised ref stream has data after flush".into(),
3893                ));
3894            }
3895            PktLineFrame::Flush => {
3896                saw_flush = true;
3897                if idx + 1 != frames.len() {
3898                    return Err(GitError::InvalidFormat(
3899                        "advertised ref stream has frames after flush".into(),
3900                    ));
3901                }
3902            }
3903            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
3904                return Err(GitError::InvalidFormat(
3905                    "advertised ref stream contains a non-flush control packet".into(),
3906                ));
3907            }
3908        }
3909    }
3910    if !saw_flush {
3911        return Err(GitError::InvalidFormat(
3912            "advertised ref stream missing flush".into(),
3913        ));
3914    }
3915    Ok(set)
3916}
3917
3918pub fn encode_ref_advertisements(advertisements: &[RefAdvertisement]) -> Result<Vec<PktLineFrame>> {
3919    encode_ref_advertisement_set(&RefAdvertisementSet {
3920        protocol: ProtocolVersion::V0,
3921        refs: advertisements.to_vec(),
3922        shallow: Vec::new(),
3923    })
3924}
3925
3926pub fn encode_ref_advertisement_set(set: &RefAdvertisementSet) -> Result<Vec<PktLineFrame>> {
3927    let mut frames = Vec::new();
3928    match set.protocol {
3929        ProtocolVersion::V0 => {}
3930        ProtocolVersion::V1 => frames.push(PktLineFrame::data(line_from_str("version 1"))?),
3931        ProtocolVersion::V2 => {
3932            return Err(GitError::InvalidFormat(
3933                "protocol v2 does not use v0/v1 advertised-ref streams".into(),
3934            ));
3935        }
3936    }
3937    if set.refs.is_empty() && !set.shallow.is_empty() {
3938        return Err(GitError::InvalidFormat(
3939            "advertised shallow refs require advertised refs".into(),
3940        ));
3941    }
3942    for (idx, advertisement) in set.refs.iter().enumerate() {
3943        if idx != 0 && !advertisement.capabilities.is_empty() {
3944            return Err(GitError::InvalidFormat(
3945                "advertised ref capabilities must appear on the first ref".into(),
3946            ));
3947        }
3948        frames.push(PktLineFrame::data(encode_ref_advertisement(
3949            advertisement,
3950        )?)?);
3951    }
3952    for oid in &set.shallow {
3953        frames.push(PktLineFrame::data(line_from_str(&format!(
3954            "shallow {oid}"
3955        )))?);
3956    }
3957    frames.push(PktLineFrame::Flush);
3958    Ok(frames)
3959}
3960
3961pub fn read_ref_advertisements(
3962    format: ObjectFormat,
3963    reader: &mut impl Read,
3964) -> Result<Vec<RefAdvertisement>> {
3965    let frames = read_pkt_line_frames_until_flush(reader)?;
3966    parse_ref_advertisements(format, &frames)
3967}
3968
3969pub fn read_ref_advertisement_set(
3970    format: ObjectFormat,
3971    reader: &mut impl Read,
3972) -> Result<RefAdvertisementSet> {
3973    let frames = read_pkt_line_frames_until_flush(reader)?;
3974    parse_ref_advertisement_set(format, &frames)
3975}
3976
3977pub fn write_ref_advertisements(
3978    writer: &mut impl Write,
3979    advertisements: &[RefAdvertisement],
3980) -> Result<()> {
3981    write_ref_advertisement_stream(writer, ProtocolVersion::V0, advertisements, &[])
3982}
3983
3984pub fn write_ref_advertisement_set(
3985    writer: &mut impl Write,
3986    set: &RefAdvertisementSet,
3987) -> Result<()> {
3988    write_ref_advertisement_stream(writer, set.protocol, &set.refs, &set.shallow)
3989}
3990
3991fn write_ref_advertisement_stream(
3992    writer: &mut impl Write,
3993    protocol: ProtocolVersion,
3994    refs: &[RefAdvertisement],
3995    shallow: &[ObjectId],
3996) -> Result<()> {
3997    match protocol {
3998        ProtocolVersion::V0 => {}
3999        ProtocolVersion::V1 => write_pkt_line_payload(writer, b"version 1\n")?,
4000        ProtocolVersion::V2 => {
4001            return Err(GitError::InvalidFormat(
4002                "protocol v2 does not use v0/v1 advertised-ref streams".into(),
4003            ));
4004        }
4005    }
4006    if refs.is_empty() && !shallow.is_empty() {
4007        return Err(GitError::InvalidFormat(
4008            "advertised shallow refs require advertised refs".into(),
4009        ));
4010    }
4011    for (idx, advertisement) in refs.iter().enumerate() {
4012        if idx != 0 && !advertisement.capabilities.is_empty() {
4013            return Err(GitError::InvalidFormat(
4014                "advertised ref capabilities must appear on the first ref".into(),
4015            ));
4016        }
4017        write_pkt_line_payload(writer, &encode_ref_advertisement(advertisement)?)?;
4018    }
4019    for oid in shallow {
4020        write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
4021    }
4022    writer.write_all(b"0000")?;
4023    Ok(())
4024}
4025
4026pub fn parse_dumb_http_info_refs(
4027    format: ObjectFormat,
4028    input: &[u8],
4029) -> Result<Vec<DumbHttpRefRecord>> {
4030    if input.is_empty() {
4031        return Ok(Vec::new());
4032    }
4033    input
4034        .split_inclusive(|byte| *byte == b'\n')
4035        .map(|line| parse_dumb_http_info_ref_record(format, line))
4036        .collect()
4037}
4038
4039pub fn encode_dumb_http_info_refs(records: &[DumbHttpRefRecord]) -> Result<Vec<u8>> {
4040    let mut out = Vec::new();
4041    for record in records {
4042        validate_dumb_http_ref_name(&record.name)?;
4043        out.extend_from_slice(record.oid.to_string().as_bytes());
4044        out.push(b'\t');
4045        out.extend_from_slice(record.name.as_bytes());
4046        if record.peeled {
4047            out.extend_from_slice(b"^{}");
4048        }
4049        out.push(b'\n');
4050    }
4051    Ok(out)
4052}
4053
4054pub fn read_dumb_http_info_refs(
4055    format: ObjectFormat,
4056    reader: &mut impl Read,
4057) -> Result<Vec<DumbHttpRefRecord>> {
4058    let mut input = Vec::new();
4059    reader.read_to_end(&mut input)?;
4060    parse_dumb_http_info_refs(format, &input)
4061}
4062
4063pub fn write_dumb_http_info_refs(
4064    writer: &mut impl Write,
4065    records: &[DumbHttpRefRecord],
4066) -> Result<()> {
4067    for record in records {
4068        validate_dumb_http_ref_name(&record.name)?;
4069        writer.write_all(record.oid.to_string().as_bytes())?;
4070        writer.write_all(b"\t")?;
4071        writer.write_all(record.name.as_bytes())?;
4072        if record.peeled {
4073            writer.write_all(b"^{}")?;
4074        }
4075        writer.write_all(b"\n")?;
4076    }
4077    Ok(())
4078}
4079
4080pub fn parse_dumb_http_alternates(input: &[u8]) -> Result<Vec<String>> {
4081    if input.is_empty() {
4082        return Ok(Vec::new());
4083    }
4084    input
4085        .split_inclusive(|byte| *byte == b'\n')
4086        .map(parse_dumb_http_alternate)
4087        .collect()
4088}
4089
4090pub fn encode_dumb_http_alternates(alternates: &[String]) -> Result<Vec<u8>> {
4091    let mut out = Vec::new();
4092    for alternate in alternates {
4093        validate_dumb_http_alternate(alternate)?;
4094        out.extend_from_slice(alternate.as_bytes());
4095        out.push(b'\n');
4096    }
4097    Ok(out)
4098}
4099
4100pub fn read_dumb_http_alternates(reader: &mut impl Read) -> Result<Vec<String>> {
4101    let mut input = Vec::new();
4102    reader.read_to_end(&mut input)?;
4103    parse_dumb_http_alternates(&input)
4104}
4105
4106pub fn write_dumb_http_alternates(writer: &mut impl Write, alternates: &[String]) -> Result<()> {
4107    for alternate in alternates {
4108        validate_dumb_http_alternate(alternate)?;
4109        writer.write_all(alternate.as_bytes())?;
4110        writer.write_all(b"\n")?;
4111    }
4112    Ok(())
4113}
4114
4115pub fn parse_dumb_http_packs(
4116    format: ObjectFormat,
4117    input: &[u8],
4118) -> Result<Vec<DumbHttpPackRecord>> {
4119    if input.is_empty() {
4120        return Ok(Vec::new());
4121    }
4122    input
4123        .split_inclusive(|byte| *byte == b'\n')
4124        .map(|line| parse_dumb_http_pack_record(format, line))
4125        .collect()
4126}
4127
4128pub fn encode_dumb_http_packs(records: &[DumbHttpPackRecord]) -> Result<Vec<u8>> {
4129    let mut out = Vec::new();
4130    for record in records {
4131        out.extend_from_slice(format!("P pack-{}.pack\n", record.hash).as_bytes());
4132    }
4133    Ok(out)
4134}
4135
4136pub fn read_dumb_http_packs(
4137    format: ObjectFormat,
4138    reader: &mut impl Read,
4139) -> Result<Vec<DumbHttpPackRecord>> {
4140    let mut input = Vec::new();
4141    reader.read_to_end(&mut input)?;
4142    parse_dumb_http_packs(format, &input)
4143}
4144
4145pub fn write_dumb_http_packs(
4146    writer: &mut impl Write,
4147    records: &[DumbHttpPackRecord],
4148) -> Result<()> {
4149    for record in records {
4150        writer.write_all(format!("P pack-{}.pack\n", record.hash).as_bytes())?;
4151    }
4152    Ok(())
4153}
4154
4155pub fn parse_upload_pack_request(
4156    format: ObjectFormat,
4157    frames: &[PktLineFrame],
4158) -> Result<Option<UploadPackRequest>> {
4159    if matches!(frames, [PktLineFrame::Flush]) {
4160        return Ok(None);
4161    }
4162
4163    let mut request = UploadPackRequest::default();
4164    let mut in_options = false;
4165    let mut saw_flush = false;
4166    for (idx, frame) in frames.iter().enumerate() {
4167        match frame {
4168            PktLineFrame::Data(payload) if !saw_flush => {
4169                let text = parse_protocol_v2_line_text("upload-pack request line", payload)?;
4170                if let Some(value) = text.strip_prefix("want ") {
4171                    if in_options {
4172                        return Err(GitError::InvalidFormat(
4173                            "upload-pack request has want after options".into(),
4174                        ));
4175                    }
4176                    let (oid, capabilities) = if request.wants.is_empty() {
4177                        value
4178                            .split_once(' ')
4179                            .map_or((value, None), |(oid, caps)| (oid, Some(caps.as_bytes())))
4180                    } else {
4181                        if value.contains(' ') {
4182                            return Err(GitError::InvalidFormat(
4183                                "additional upload-pack want has capabilities".into(),
4184                            ));
4185                        }
4186                        (value, None)
4187                    };
4188                    validate_protocol_v2_token("upload-pack want", oid)?;
4189                    request.wants.push(ObjectId::from_hex(format, oid)?);
4190                    if let Some(capabilities) = capabilities {
4191                        request.capabilities = parse_capabilities(capabilities)?;
4192                    }
4193                    continue;
4194                }
4195
4196                if request.wants.is_empty() {
4197                    return Err(GitError::InvalidFormat(
4198                        "upload-pack request must start with want".into(),
4199                    ));
4200                }
4201                in_options = true;
4202                if text.starts_with("shallow ") {
4203                    request.shallow.push(parse_oid_argument(
4204                        format,
4205                        "upload-pack shallow",
4206                        text,
4207                        "shallow ",
4208                    )?);
4209                } else if text.starts_with("deepen ") {
4210                    if request.deepen.is_some() {
4211                        return Err(GitError::InvalidFormat(
4212                            "upload-pack request has duplicate deepen".into(),
4213                        ));
4214                    }
4215                    request.deepen =
4216                        Some(parse_u32_argument("upload-pack deepen", text, "deepen ")?);
4217                } else if text.starts_with("deepen-since ") {
4218                    if request.deepen_since.is_some() {
4219                        return Err(GitError::InvalidFormat(
4220                            "upload-pack request has duplicate deepen-since".into(),
4221                        ));
4222                    }
4223                    request.deepen_since = Some(parse_u64_argument(
4224                        "upload-pack deepen-since",
4225                        text,
4226                        "deepen-since ",
4227                    )?);
4228                } else if let Some(name) = text.strip_prefix("deepen-not ") {
4229                    validate_protocol_v2_token("upload-pack deepen-not", name)?;
4230                    request.deepen_not.push(name.to_string());
4231                } else if let Some(filter) = text.strip_prefix("filter ") {
4232                    if request.filter.is_some() {
4233                        return Err(GitError::InvalidFormat(
4234                            "upload-pack request has duplicate filter".into(),
4235                        ));
4236                    }
4237                    validate_protocol_v2_token("upload-pack filter", filter)?;
4238                    request.filter = Some(filter.to_string());
4239                } else {
4240                    return Err(GitError::InvalidFormat(format!(
4241                        "unsupported upload-pack request line {text}"
4242                    )));
4243                }
4244            }
4245            PktLineFrame::Data(_) => {
4246                return Err(GitError::InvalidFormat(
4247                    "upload-pack request has data after flush".into(),
4248                ));
4249            }
4250            PktLineFrame::Flush => {
4251                saw_flush = true;
4252                if idx + 1 != frames.len() {
4253                    return Err(GitError::InvalidFormat(
4254                        "upload-pack request has frames after flush".into(),
4255                    ));
4256                }
4257            }
4258            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4259                return Err(GitError::InvalidFormat(
4260                    "upload-pack request contains a non-flush control packet".into(),
4261                ));
4262            }
4263        }
4264    }
4265    if !saw_flush {
4266        return Err(GitError::InvalidFormat(
4267            "upload-pack request missing flush".into(),
4268        ));
4269    }
4270    if request.wants.is_empty() {
4271        return Err(GitError::InvalidFormat(
4272            "upload-pack request missing want".into(),
4273        ));
4274    }
4275    Ok(Some(request))
4276}
4277
4278pub fn encode_upload_pack_request(
4279    request: Option<&UploadPackRequest>,
4280) -> Result<Vec<PktLineFrame>> {
4281    let Some(request) = request else {
4282        return Ok(vec![PktLineFrame::Flush]);
4283    };
4284    if request.wants.is_empty() {
4285        return Err(GitError::InvalidFormat(
4286            "upload-pack request missing want".into(),
4287        ));
4288    }
4289
4290    let mut frames = Vec::new();
4291    for (idx, oid) in request.wants.iter().enumerate() {
4292        let mut line = format!("want {oid}");
4293        if idx == 0 && !request.capabilities.is_empty() {
4294            line.push(' ');
4295            line.push_str(
4296                &String::from_utf8(encode_capabilities(&request.capabilities)?)
4297                    .map_err(|err| GitError::InvalidFormat(err.to_string()))?,
4298            );
4299        }
4300        frames.push(PktLineFrame::data(line_from_str(&line))?);
4301    }
4302    for oid in &request.shallow {
4303        frames.push(PktLineFrame::data(line_from_str(&format!(
4304            "shallow {oid}"
4305        )))?);
4306    }
4307    if let Some(deepen) = request.deepen {
4308        if deepen == 0 {
4309            return Err(GitError::InvalidFormat(
4310                "upload-pack deepen must be positive".into(),
4311            ));
4312        }
4313        frames.push(PktLineFrame::data(line_from_str(&format!(
4314            "deepen {deepen}"
4315        )))?);
4316    }
4317    if let Some(deepen_since) = request.deepen_since {
4318        frames.push(PktLineFrame::data(line_from_str(&format!(
4319            "deepen-since {deepen_since}"
4320        )))?);
4321    }
4322    for name in &request.deepen_not {
4323        validate_protocol_v2_token("upload-pack deepen-not", name)?;
4324        frames.push(PktLineFrame::data(line_from_str(&format!(
4325            "deepen-not {name}"
4326        )))?);
4327    }
4328    if let Some(filter) = &request.filter {
4329        validate_protocol_v2_token("upload-pack filter", filter)?;
4330        frames.push(PktLineFrame::data(line_from_str(&format!(
4331            "filter {filter}"
4332        )))?);
4333    }
4334    frames.push(PktLineFrame::Flush);
4335    Ok(frames)
4336}
4337
4338pub fn read_upload_pack_request(
4339    format: ObjectFormat,
4340    reader: &mut impl Read,
4341) -> Result<Option<UploadPackRequest>> {
4342    let frames = read_pkt_line_frames_until_flush(reader)?;
4343    parse_upload_pack_request(format, &frames)
4344}
4345
4346pub fn write_upload_pack_request(
4347    writer: &mut impl Write,
4348    request: Option<&UploadPackRequest>,
4349) -> Result<()> {
4350    let Some(request) = request else {
4351        writer.write_all(b"0000")?;
4352        return Ok(());
4353    };
4354    if request.wants.is_empty() {
4355        return Err(GitError::InvalidFormat(
4356            "upload-pack request missing want".into(),
4357        ));
4358    }
4359
4360    for (idx, oid) in request.wants.iter().enumerate() {
4361        let mut line = format!("want {oid}");
4362        if idx == 0 && !request.capabilities.is_empty() {
4363            line.push(' ');
4364            line.push_str(
4365                &String::from_utf8(encode_capabilities(&request.capabilities)?)
4366                    .map_err(|err| GitError::InvalidFormat(err.to_string()))?,
4367            );
4368        }
4369        write_pkt_line_payload(writer, &line_from_str(&line))?;
4370    }
4371    for oid in &request.shallow {
4372        write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
4373    }
4374    if let Some(deepen) = request.deepen {
4375        if deepen == 0 {
4376            return Err(GitError::InvalidFormat(
4377                "upload-pack deepen must be positive".into(),
4378            ));
4379        }
4380        write_pkt_line_payload(writer, &line_from_str(&format!("deepen {deepen}")))?;
4381    }
4382    if let Some(deepen_since) = request.deepen_since {
4383        write_pkt_line_payload(
4384            writer,
4385            &line_from_str(&format!("deepen-since {deepen_since}")),
4386        )?;
4387    }
4388    for name in &request.deepen_not {
4389        validate_protocol_v2_token("upload-pack deepen-not", name)?;
4390        write_pkt_line_payload(writer, &line_from_str(&format!("deepen-not {name}")))?;
4391    }
4392    if let Some(filter) = &request.filter {
4393        validate_protocol_v2_token("upload-pack filter", filter)?;
4394        write_pkt_line_payload(writer, &line_from_str(&format!("filter {filter}")))?;
4395    }
4396    writer.write_all(b"0000")?;
4397    Ok(())
4398}
4399
4400pub fn parse_upload_pack_features(capabilities: &[Capability]) -> Result<UploadPackFeatures> {
4401    let mut features = UploadPackFeatures::default();
4402    for capability in capabilities {
4403        match capability.name.as_str() {
4404            "multi_ack" => set_upload_pack_flag(&mut features.multi_ack, capability)?,
4405            "multi_ack_detailed" => {
4406                set_upload_pack_flag(&mut features.multi_ack_detailed, capability)?
4407            }
4408            "no-done" => set_upload_pack_flag(&mut features.no_done, capability)?,
4409            "thin-pack" => set_upload_pack_flag(&mut features.thin_pack, capability)?,
4410            "side-band" => set_upload_pack_flag(&mut features.side_band, capability)?,
4411            "side-band-64k" => set_upload_pack_flag(&mut features.side_band_64k, capability)?,
4412            "ofs-delta" => set_upload_pack_flag(&mut features.ofs_delta, capability)?,
4413            "shallow" => set_upload_pack_flag(&mut features.shallow, capability)?,
4414            "deepen-since" => set_upload_pack_flag(&mut features.deepen_since, capability)?,
4415            "deepen-not" => set_upload_pack_flag(&mut features.deepen_not, capability)?,
4416            "include-tag" => set_upload_pack_flag(&mut features.include_tag, capability)?,
4417            "no-progress" => set_upload_pack_flag(&mut features.no_progress, capability)?,
4418            "allow-tip-sha1-in-want" => {
4419                set_upload_pack_flag(&mut features.allow_tip_sha1_in_want, capability)?
4420            }
4421            "allow-reachable-sha1-in-want" => {
4422                set_upload_pack_flag(&mut features.allow_reachable_sha1_in_want, capability)?
4423            }
4424            "filter" => set_upload_pack_flag(&mut features.filter, capability)?,
4425            "agent" => {
4426                let Some(agent) = &capability.value else {
4427                    return Err(GitError::InvalidFormat(
4428                        "upload-pack agent capability is missing value".into(),
4429                    ));
4430                };
4431                if features.agent.is_some() {
4432                    return Err(GitError::InvalidFormat(
4433                        "upload-pack has duplicate agent capability".into(),
4434                    ));
4435                }
4436                validate_capability_field("upload-pack agent", agent)?;
4437                features.agent = Some(agent.clone());
4438            }
4439            "object-format" => {
4440                let Some(format) = &capability.value else {
4441                    return Err(GitError::InvalidFormat(
4442                        "upload-pack object-format capability is missing value".into(),
4443                    ));
4444                };
4445                if features.object_format.is_some() {
4446                    return Err(GitError::InvalidFormat(
4447                        "upload-pack has duplicate object-format capability".into(),
4448                    ));
4449                }
4450                validate_capability_field("upload-pack object-format", format)?;
4451                features.object_format = Some(format.parse()?);
4452            }
4453            "symref" => {
4454                let Some(symref) = &capability.value else {
4455                    continue;
4456                };
4457                validate_capability_field("upload-pack symref", symref)?;
4458                features.symrefs.push(symref.clone());
4459            }
4460            _ => {
4461                encode_capabilities(std::slice::from_ref(capability))?;
4462                if features
4463                    .unknown
4464                    .iter()
4465                    .any(|known| known.name == capability.name)
4466                {
4467                    return Err(GitError::InvalidFormat(format!(
4468                        "upload-pack has duplicate {} capability",
4469                        capability.name
4470                    )));
4471                }
4472                features.unknown.push(capability.clone());
4473            }
4474        }
4475    }
4476    Ok(features)
4477}
4478
4479pub fn encode_upload_pack_features(features: &UploadPackFeatures) -> Result<Vec<Capability>> {
4480    let mut capabilities = Vec::new();
4481    push_upload_pack_flag(&mut capabilities, "multi_ack", features.multi_ack);
4482    push_upload_pack_flag(
4483        &mut capabilities,
4484        "multi_ack_detailed",
4485        features.multi_ack_detailed,
4486    );
4487    push_upload_pack_flag(&mut capabilities, "no-done", features.no_done);
4488    push_upload_pack_flag(&mut capabilities, "thin-pack", features.thin_pack);
4489    push_upload_pack_flag(&mut capabilities, "side-band", features.side_band);
4490    push_upload_pack_flag(&mut capabilities, "side-band-64k", features.side_band_64k);
4491    push_upload_pack_flag(&mut capabilities, "ofs-delta", features.ofs_delta);
4492    push_upload_pack_flag(&mut capabilities, "shallow", features.shallow);
4493    push_upload_pack_flag(&mut capabilities, "deepen-since", features.deepen_since);
4494    push_upload_pack_flag(&mut capabilities, "deepen-not", features.deepen_not);
4495    push_upload_pack_flag(&mut capabilities, "include-tag", features.include_tag);
4496    push_upload_pack_flag(&mut capabilities, "no-progress", features.no_progress);
4497    push_upload_pack_flag(
4498        &mut capabilities,
4499        "allow-tip-sha1-in-want",
4500        features.allow_tip_sha1_in_want,
4501    );
4502    push_upload_pack_flag(
4503        &mut capabilities,
4504        "allow-reachable-sha1-in-want",
4505        features.allow_reachable_sha1_in_want,
4506    );
4507    push_upload_pack_flag(&mut capabilities, "filter", features.filter);
4508    if let Some(agent) = &features.agent {
4509        validate_capability_field("upload-pack agent", agent)?;
4510        capabilities.push(Capability {
4511            name: "agent".into(),
4512            value: Some(agent.clone()),
4513        });
4514    }
4515    if let Some(format) = features.object_format {
4516        capabilities.push(Capability {
4517            name: "object-format".into(),
4518            value: Some(format.name().into()),
4519        });
4520    }
4521    for symref in &features.symrefs {
4522        validate_capability_field("upload-pack symref", symref)?;
4523        capabilities.push(Capability {
4524            name: "symref".into(),
4525            value: Some(symref.clone()),
4526        });
4527    }
4528    for capability in &features.unknown {
4529        if is_known_upload_pack_capability(&capability.name) {
4530            return Err(GitError::InvalidFormat(format!(
4531                "upload-pack unknown capability duplicates known capability {}",
4532                capability.name
4533            )));
4534        }
4535        encode_capabilities(std::slice::from_ref(capability))?;
4536        capabilities.push(capability.clone());
4537    }
4538    Ok(capabilities)
4539}
4540
4541pub fn validate_upload_pack_request_features(
4542    features: &UploadPackFeatures,
4543    request: &UploadPackRequest,
4544) -> Result<()> {
4545    for capability in &request.capabilities {
4546        if is_upload_pack_flag_capability(&capability.name) {
4547            reject_capability_value("upload-pack request capability", capability)?;
4548        }
4549        match capability.name.as_str() {
4550            "multi_ack" if !features.multi_ack => {
4551                return Err(GitError::InvalidFormat(
4552                    "upload-pack request uses multi_ack without advertised capability".into(),
4553                ));
4554            }
4555            "multi_ack_detailed" if !features.multi_ack_detailed => {
4556                return Err(GitError::InvalidFormat(
4557                    "upload-pack request uses multi_ack_detailed without advertised capability"
4558                        .into(),
4559                ));
4560            }
4561            "no-done" if !features.no_done => {
4562                return Err(GitError::InvalidFormat(
4563                    "upload-pack request uses no-done without advertised capability".into(),
4564                ));
4565            }
4566            "thin-pack" if !features.thin_pack => {
4567                return Err(GitError::InvalidFormat(
4568                    "upload-pack request uses thin-pack without advertised capability".into(),
4569                ));
4570            }
4571            "side-band" if !features.side_band => {
4572                return Err(GitError::InvalidFormat(
4573                    "upload-pack request uses side-band without advertised capability".into(),
4574                ));
4575            }
4576            "side-band-64k" if !features.side_band_64k => {
4577                return Err(GitError::InvalidFormat(
4578                    "upload-pack request uses side-band-64k without advertised capability".into(),
4579                ));
4580            }
4581            "ofs-delta" if !features.ofs_delta => {
4582                return Err(GitError::InvalidFormat(
4583                    "upload-pack request uses ofs-delta without advertised capability".into(),
4584                ));
4585            }
4586            "include-tag" if !features.include_tag => {
4587                return Err(GitError::InvalidFormat(
4588                    "upload-pack request uses include-tag without advertised capability".into(),
4589                ));
4590            }
4591            "no-progress" if !features.no_progress => {
4592                return Err(GitError::InvalidFormat(
4593                    "upload-pack request uses no-progress without advertised capability".into(),
4594                ));
4595            }
4596            "allow-tip-sha1-in-want" if !features.allow_tip_sha1_in_want => {
4597                return Err(GitError::InvalidFormat(
4598                    "upload-pack request uses allow-tip-sha1-in-want without advertised capability"
4599                        .into(),
4600                ));
4601            }
4602            "allow-reachable-sha1-in-want" if !features.allow_reachable_sha1_in_want => {
4603                return Err(GitError::InvalidFormat(
4604                    "upload-pack request uses allow-reachable-sha1-in-want without advertised capability"
4605                        .into(),
4606                ));
4607            }
4608            "filter" if !features.filter => {
4609                return Err(GitError::InvalidFormat(
4610                    "upload-pack request uses filter capability without advertised capability"
4611                        .into(),
4612                ));
4613            }
4614            "agent" => {
4615                let Some(agent) = &capability.value else {
4616                    return Err(GitError::InvalidFormat(
4617                        "upload-pack request agent capability is missing value".into(),
4618                    ));
4619                };
4620                validate_capability_field("upload-pack request agent", agent)?;
4621            }
4622            "object-format" => {
4623                let Some(format) = &capability.value else {
4624                    return Err(GitError::InvalidFormat(
4625                        "upload-pack request object-format capability is missing value".into(),
4626                    ));
4627                };
4628                let requested_format: ObjectFormat = format.parse()?;
4629                if features.object_format != Some(requested_format) {
4630                    return Err(GitError::InvalidFormat(
4631                        "upload-pack request object-format was not advertised".into(),
4632                    ));
4633                }
4634            }
4635            name if is_known_upload_pack_capability(name) => {}
4636            _ => {
4637                if !features
4638                    .unknown
4639                    .iter()
4640                    .any(|advertised| advertised.name == capability.name)
4641                {
4642                    return Err(GitError::InvalidFormat(format!(
4643                        "upload-pack request uses unadvertised capability {}",
4644                        capability.name
4645                    )));
4646                }
4647            }
4648        }
4649    }
4650
4651    let sideband = request
4652        .capabilities
4653        .iter()
4654        .any(|capability| capability.name == "side-band");
4655    let sideband_64k = request
4656        .capabilities
4657        .iter()
4658        .any(|capability| capability.name == "side-band-64k");
4659    if sideband && sideband_64k {
4660        return Err(GitError::InvalidFormat(
4661            "upload-pack request must not request both side-band and side-band-64k".into(),
4662        ));
4663    }
4664
4665    if !features.shallow && (!request.shallow.is_empty() || request.deepen.is_some()) {
4666        return Err(GitError::InvalidFormat(
4667            "upload-pack request uses shallow/deepen without advertised shallow capability".into(),
4668        ));
4669    }
4670    if !features.deepen_since && request.deepen_since.is_some() {
4671        return Err(GitError::InvalidFormat(
4672            "upload-pack request uses deepen-since without advertised capability".into(),
4673        ));
4674    }
4675    if !features.deepen_not && !request.deepen_not.is_empty() {
4676        return Err(GitError::InvalidFormat(
4677            "upload-pack request uses deepen-not without advertised capability".into(),
4678        ));
4679    }
4680    if !features.filter && request.filter.is_some() {
4681        return Err(GitError::InvalidFormat(
4682            "upload-pack request uses filter without advertised capability".into(),
4683        ));
4684    }
4685    Ok(())
4686}
4687
4688pub fn build_upload_pack_raw_packfile_response<C, B>(
4689    features: &UploadPackFeatures,
4690    request: UploadPackRequest,
4691    haves: impl IntoIterator<Item = ObjectId>,
4692    mut contains_object: C,
4693    mut build_pack: B,
4694) -> Result<UploadPackRawPackfileResponse>
4695where
4696    C: FnMut(&ObjectId) -> Result<bool>,
4697    B: FnMut(Vec<ObjectId>, Vec<ObjectId>) -> Result<Option<Vec<u8>>>,
4698{
4699    validate_upload_pack_request_features(features, &request)?;
4700    for want in &request.wants {
4701        if !contains_object(want)? {
4702            return Err(GitError::InvalidObject(format!(
4703                "upload-pack requested missing object {want}"
4704            )));
4705        }
4706    }
4707    let known_haves = haves
4708        .into_iter()
4709        .filter_map(|oid| match contains_object(&oid) {
4710            Ok(true) => Some(Ok(oid)),
4711            Ok(false) => None,
4712            Err(err) => Some(Err(err)),
4713        })
4714        .collect::<Result<Vec<_>>>()?;
4715    let packfile = build_pack(request.wants, known_haves)?
4716        .ok_or_else(|| GitError::InvalidObject("upload-pack request produced empty pack".into()))?;
4717    Ok(UploadPackRawPackfileResponse {
4718        acknowledgments: vec![UploadPackAcknowledgment::Nak],
4719        packfile,
4720    })
4721}
4722
4723pub fn parse_upload_pack_shallow_update(
4724    format: ObjectFormat,
4725    frames: &[PktLineFrame],
4726) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
4727    let mut entries = Vec::new();
4728    let mut saw_flush = false;
4729    for (idx, frame) in frames.iter().enumerate() {
4730        match frame {
4731            PktLineFrame::Data(payload) if !saw_flush => {
4732                entries.push(parse_fetch_shallow_info(format, payload)?);
4733            }
4734            PktLineFrame::Data(_) => {
4735                return Err(GitError::InvalidFormat(
4736                    "upload-pack shallow update has data after flush".into(),
4737                ));
4738            }
4739            PktLineFrame::Flush => {
4740                saw_flush = true;
4741                if idx + 1 != frames.len() {
4742                    return Err(GitError::InvalidFormat(
4743                        "upload-pack shallow update has frames after flush".into(),
4744                    ));
4745                }
4746            }
4747            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4748                return Err(GitError::InvalidFormat(
4749                    "upload-pack shallow update contains a non-flush control packet".into(),
4750                ));
4751            }
4752        }
4753    }
4754    if !saw_flush {
4755        return Err(GitError::InvalidFormat(
4756            "upload-pack shallow update missing flush".into(),
4757        ));
4758    }
4759    Ok(entries)
4760}
4761
4762pub fn encode_upload_pack_shallow_update(
4763    entries: &[ProtocolV2FetchShallowInfo],
4764) -> Result<Vec<PktLineFrame>> {
4765    let mut frames = Vec::new();
4766    for entry in entries {
4767        let line = match entry {
4768            ProtocolV2FetchShallowInfo::Shallow(oid) => format!("shallow {oid}"),
4769            ProtocolV2FetchShallowInfo::Unshallow(oid) => format!("unshallow {oid}"),
4770        };
4771        frames.push(PktLineFrame::data(line_from_str(&line))?);
4772    }
4773    frames.push(PktLineFrame::Flush);
4774    Ok(frames)
4775}
4776
4777pub fn read_upload_pack_shallow_update(
4778    format: ObjectFormat,
4779    reader: &mut impl Read,
4780) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
4781    let frames = read_pkt_line_frames_until_flush(reader)?;
4782    parse_upload_pack_shallow_update(format, &frames)
4783}
4784
4785pub fn write_upload_pack_shallow_update(
4786    writer: &mut impl Write,
4787    entries: &[ProtocolV2FetchShallowInfo],
4788) -> Result<()> {
4789    for entry in entries {
4790        let line = match entry {
4791            ProtocolV2FetchShallowInfo::Shallow(oid) => format!("shallow {oid}"),
4792            ProtocolV2FetchShallowInfo::Unshallow(oid) => format!("unshallow {oid}"),
4793        };
4794        write_pkt_line_payload(writer, &line_from_str(&line))?;
4795    }
4796    writer.write_all(b"0000")?;
4797    Ok(())
4798}
4799
4800pub fn parse_upload_pack_negotiation_request(
4801    format: ObjectFormat,
4802    frames: &[PktLineFrame],
4803) -> Result<UploadPackNegotiationRequest> {
4804    let mut request = UploadPackNegotiationRequest::default();
4805    let mut terminated = false;
4806    for (idx, frame) in frames.iter().enumerate() {
4807        match frame {
4808            PktLineFrame::Data(payload) if !terminated => {
4809                let text = parse_protocol_v2_line_text("upload-pack negotiation line", payload)?;
4810                if text == "done" {
4811                    request.done = true;
4812                    terminated = true;
4813                    if idx + 1 != frames.len() {
4814                        return Err(GitError::InvalidFormat(
4815                            "upload-pack negotiation has frames after done".into(),
4816                        ));
4817                    }
4818                } else if text.starts_with("have ") {
4819                    request.haves.push(parse_oid_argument(
4820                        format,
4821                        "upload-pack have",
4822                        text,
4823                        "have ",
4824                    )?);
4825                } else {
4826                    return Err(GitError::InvalidFormat(format!(
4827                        "unsupported upload-pack negotiation line {text}"
4828                    )));
4829                }
4830            }
4831            PktLineFrame::Data(_) => {
4832                return Err(GitError::InvalidFormat(
4833                    "upload-pack negotiation has data after terminator".into(),
4834                ));
4835            }
4836            PktLineFrame::Flush => {
4837                terminated = true;
4838                if idx + 1 != frames.len() {
4839                    return Err(GitError::InvalidFormat(
4840                        "upload-pack negotiation has frames after flush".into(),
4841                    ));
4842                }
4843            }
4844            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4845                return Err(GitError::InvalidFormat(
4846                    "upload-pack negotiation contains a non-flush control packet".into(),
4847                ));
4848            }
4849        }
4850    }
4851    if !terminated {
4852        return Err(GitError::InvalidFormat(
4853            "upload-pack negotiation missing terminator".into(),
4854        ));
4855    }
4856    Ok(request)
4857}
4858
4859pub fn encode_upload_pack_negotiation_request(
4860    request: &UploadPackNegotiationRequest,
4861) -> Result<Vec<PktLineFrame>> {
4862    let mut frames = Vec::new();
4863    for oid in &request.haves {
4864        frames.push(PktLineFrame::data(line_from_str(&format!("have {oid}")))?);
4865    }
4866    if request.done {
4867        frames.push(PktLineFrame::data(line_from_str("done"))?);
4868    } else {
4869        frames.push(PktLineFrame::Flush);
4870    }
4871    Ok(frames)
4872}
4873
4874pub fn read_upload_pack_negotiation_request(
4875    format: ObjectFormat,
4876    reader: &mut impl Read,
4877) -> Result<UploadPackNegotiationRequest> {
4878    let mut frames = Vec::new();
4879    loop {
4880        let Some(frame) = read_pkt_line_frame(reader)? else {
4881            return Err(GitError::InvalidFormat(
4882                "pkt-line stream ended before upload-pack negotiation terminator".into(),
4883            ));
4884        };
4885        let done = match &frame {
4886            PktLineFrame::Flush => true,
4887            PktLineFrame::Data(payload) => trim_trailing_lf(payload) == b"done",
4888            _ => false,
4889        };
4890        frames.push(frame);
4891        if done {
4892            return parse_upload_pack_negotiation_request(format, &frames);
4893        }
4894    }
4895}
4896
4897pub fn write_upload_pack_negotiation_request(
4898    writer: &mut impl Write,
4899    request: &UploadPackNegotiationRequest,
4900) -> Result<()> {
4901    for oid in &request.haves {
4902        write_pkt_line_payload(writer, &line_from_str(&format!("have {oid}")))?;
4903    }
4904    if request.done {
4905        write_pkt_line_payload(writer, b"done\n")?;
4906    } else {
4907        writer.write_all(b"0000")?;
4908    }
4909    Ok(())
4910}
4911
4912pub fn parse_upload_pack_acknowledgment(
4913    format: ObjectFormat,
4914    payload: &[u8],
4915) -> Result<UploadPackAcknowledgment> {
4916    let text = parse_protocol_v2_line_text("upload-pack acknowledgment", payload)?;
4917    if text == "NAK" {
4918        return Ok(UploadPackAcknowledgment::Nak);
4919    }
4920    let Some(rest) = text.strip_prefix("ACK ") else {
4921        return Err(GitError::InvalidFormat(format!(
4922            "unsupported upload-pack acknowledgment {text}"
4923        )));
4924    };
4925    let mut fields = rest.split(' ');
4926    let oid = fields
4927        .next()
4928        .ok_or_else(|| GitError::InvalidFormat("upload-pack ACK missing object id".into()))?;
4929    validate_protocol_v2_token("upload-pack ACK", oid)?;
4930    let status = match fields.next() {
4931        None => None,
4932        Some("continue") => Some(UploadPackAckStatus::Continue),
4933        Some("common") => Some(UploadPackAckStatus::Common),
4934        Some("ready") => Some(UploadPackAckStatus::Ready),
4935        Some(other) => {
4936            return Err(GitError::InvalidFormat(format!(
4937                "unsupported upload-pack ACK status {other}"
4938            )));
4939        }
4940    };
4941    if fields.next().is_some() {
4942        return Err(GitError::InvalidFormat(
4943            "upload-pack ACK has too many fields".into(),
4944        ));
4945    }
4946    Ok(UploadPackAcknowledgment::Ack {
4947        oid: ObjectId::from_hex(format, oid)?,
4948        status,
4949    })
4950}
4951
4952pub fn encode_upload_pack_acknowledgment(
4953    acknowledgment: &UploadPackAcknowledgment,
4954) -> Result<Vec<u8>> {
4955    let line = match acknowledgment {
4956        UploadPackAcknowledgment::Nak => "NAK".to_string(),
4957        UploadPackAcknowledgment::Ack { oid, status } => {
4958            let mut line = format!("ACK {oid}");
4959            if let Some(status) = status {
4960                line.push(' ');
4961                line.push_str(match status {
4962                    UploadPackAckStatus::Continue => "continue",
4963                    UploadPackAckStatus::Common => "common",
4964                    UploadPackAckStatus::Ready => "ready",
4965                });
4966            }
4967            line
4968        }
4969    };
4970    Ok(line_from_str(&line))
4971}
4972
4973pub fn read_upload_pack_acknowledgment(
4974    format: ObjectFormat,
4975    reader: &mut impl Read,
4976) -> Result<UploadPackAcknowledgment> {
4977    let Some(frame) = read_pkt_line_frame(reader)? else {
4978        return Err(GitError::InvalidFormat(
4979            "pkt-line stream ended before upload-pack acknowledgment".into(),
4980        ));
4981    };
4982    match frame {
4983        PktLineFrame::Data(payload) => parse_upload_pack_acknowledgment(format, &payload),
4984        _ => Err(GitError::InvalidFormat(
4985            "upload-pack acknowledgment must be a data packet".into(),
4986        )),
4987    }
4988}
4989
4990pub fn write_upload_pack_acknowledgment(
4991    writer: &mut impl Write,
4992    acknowledgment: &UploadPackAcknowledgment,
4993) -> Result<()> {
4994    write_pkt_line_payload(writer, &encode_upload_pack_acknowledgment(acknowledgment)?)
4995}
4996
4997pub fn parse_upload_pack_packfile_response(
4998    format: ObjectFormat,
4999    frames: &[PktLineFrame],
5000) -> Result<UploadPackPackfileResponse> {
5001    let mut response = UploadPackPackfileResponse::default();
5002    let mut in_sideband = false;
5003    let mut saw_flush = false;
5004    for (idx, frame) in frames.iter().enumerate() {
5005        match frame {
5006            PktLineFrame::Data(payload) if !saw_flush => {
5007                if !in_sideband
5008                    && (trim_trailing_lf(payload) == b"NAK" || payload.starts_with(b"ACK "))
5009                {
5010                    response
5011                        .acknowledgments
5012                        .push(parse_upload_pack_acknowledgment(format, payload)?);
5013                    continue;
5014                }
5015                in_sideband = true;
5016                response.sideband.push(parse_sideband_packet(payload)?);
5017            }
5018            PktLineFrame::Data(_) => {
5019                return Err(GitError::InvalidFormat(
5020                    "upload-pack packfile response has data after flush".into(),
5021                ));
5022            }
5023            PktLineFrame::Flush => {
5024                saw_flush = true;
5025                if idx + 1 != frames.len() {
5026                    return Err(GitError::InvalidFormat(
5027                        "upload-pack packfile response has frames after flush".into(),
5028                    ));
5029                }
5030            }
5031            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5032                return Err(GitError::InvalidFormat(
5033                    "upload-pack packfile response contains a non-flush control packet".into(),
5034                ));
5035            }
5036        }
5037    }
5038    if !saw_flush {
5039        return Err(GitError::InvalidFormat(
5040            "upload-pack packfile response missing flush".into(),
5041        ));
5042    }
5043    Ok(response)
5044}
5045
5046pub fn encode_upload_pack_packfile_response(
5047    response: &UploadPackPackfileResponse,
5048) -> Result<Vec<PktLineFrame>> {
5049    let mut frames = Vec::new();
5050    for acknowledgment in &response.acknowledgments {
5051        frames.push(PktLineFrame::data(encode_upload_pack_acknowledgment(
5052            acknowledgment,
5053        )?)?);
5054    }
5055    for packet in &response.sideband {
5056        frames.push(PktLineFrame::data(encode_sideband_packet(packet)?)?);
5057    }
5058    frames.push(PktLineFrame::Flush);
5059    Ok(frames)
5060}
5061
5062pub fn read_upload_pack_packfile_response(
5063    format: ObjectFormat,
5064    reader: &mut impl Read,
5065) -> Result<UploadPackPackfileResponse> {
5066    let frames = read_pkt_line_frames_until_flush(reader)?;
5067    parse_upload_pack_packfile_response(format, &frames)
5068}
5069
5070pub fn write_upload_pack_packfile_response(
5071    writer: &mut impl Write,
5072    response: &UploadPackPackfileResponse,
5073) -> Result<()> {
5074    for acknowledgment in &response.acknowledgments {
5075        write_upload_pack_acknowledgment(writer, acknowledgment)?;
5076    }
5077    for packet in &response.sideband {
5078        write_sideband_packet(writer, packet)?;
5079    }
5080    writer.write_all(b"0000")?;
5081    Ok(())
5082}
5083
5084pub fn demux_upload_pack_packfile_response(
5085    response: &UploadPackPackfileResponse,
5086) -> Result<SideBandDemux> {
5087    demux_sideband_packets(&response.sideband)
5088}
5089
5090pub fn parse_upload_pack_raw_packfile_response(
5091    format: ObjectFormat,
5092    input: &[u8],
5093) -> Result<UploadPackRawPackfileResponse> {
5094    let mut response = UploadPackRawPackfileResponse::default();
5095    let mut offset = 0usize;
5096    while offset < input.len() {
5097        match PktLineFrame::parse(&input[offset..]) {
5098            Ok((PktLineFrame::Data(payload), consumed)) => {
5099                let trimmed = trim_trailing_lf(&payload);
5100                if trimmed == b"NAK" || trimmed.starts_with(b"ACK ") {
5101                    response
5102                        .acknowledgments
5103                        .push(parse_upload_pack_acknowledgment(format, &payload)?);
5104                    offset += consumed;
5105                    continue;
5106                }
5107                return Err(GitError::InvalidFormat(
5108                    "upload-pack raw packfile response has non-ack pkt-line before packfile".into(),
5109                ));
5110            }
5111            Ok((PktLineFrame::Flush | PktLineFrame::Delimiter | PktLineFrame::ResponseEnd, _)) => {
5112                return Err(GitError::InvalidFormat(
5113                    "upload-pack raw packfile response contains a control packet".into(),
5114                ));
5115            }
5116            Err(_) if input[offset..].starts_with(b"PACK") => break,
5117            Err(err) => return Err(err),
5118        }
5119    }
5120    response.packfile = input[offset..].to_vec();
5121    if response.packfile.is_empty() {
5122        return Err(GitError::InvalidFormat(
5123            "upload-pack raw packfile response missing packfile".into(),
5124        ));
5125    }
5126    if !response.packfile.starts_with(b"PACK") {
5127        return Err(GitError::InvalidFormat(
5128            "upload-pack raw packfile response packfile must start with PACK".into(),
5129        ));
5130    }
5131    Ok(response)
5132}
5133
5134pub fn encode_upload_pack_raw_packfile_response(
5135    response: &UploadPackRawPackfileResponse,
5136) -> Result<Vec<u8>> {
5137    if response.packfile.is_empty() {
5138        return Err(GitError::InvalidFormat(
5139            "upload-pack raw packfile response missing packfile".into(),
5140        ));
5141    }
5142    if !response.packfile.starts_with(b"PACK") {
5143        return Err(GitError::InvalidFormat(
5144            "upload-pack raw packfile response packfile must start with PACK".into(),
5145        ));
5146    }
5147    let mut out = Vec::new();
5148    for acknowledgment in &response.acknowledgments {
5149        write_pkt_line_payload(
5150            &mut out,
5151            &encode_upload_pack_acknowledgment(acknowledgment)?,
5152        )?;
5153    }
5154    out.extend_from_slice(&response.packfile);
5155    Ok(out)
5156}
5157
5158pub fn read_upload_pack_raw_packfile_response(
5159    format: ObjectFormat,
5160    reader: &mut impl Read,
5161) -> Result<UploadPackRawPackfileResponse> {
5162    let mut input = Vec::new();
5163    reader.read_to_end(&mut input)?;
5164    parse_upload_pack_raw_packfile_response(format, &input)
5165}
5166
5167pub fn read_upload_pack_raw_packfile_response_header(
5168    format: ObjectFormat,
5169    reader: &mut impl Read,
5170) -> Result<UploadPackRawPackfileResponseHeader> {
5171    let mut acknowledgments = Vec::new();
5172    loop {
5173        let mut header = [0u8; 4];
5174        reader.read_exact(&mut header)?;
5175        if &header == b"PACK" {
5176            return Ok(UploadPackRawPackfileResponseHeader {
5177                acknowledgments,
5178                pack_prefix: header.to_vec(),
5179            });
5180        }
5181        let len = parse_pkt_len(&header)?;
5182        let payload = match len {
5183            0..=2 => {
5184                return Err(GitError::InvalidFormat(
5185                    "upload-pack raw packfile response contains a control packet".into(),
5186                ));
5187            }
5188            3 => {
5189                return Err(GitError::InvalidFormat(
5190                    "reserved pkt-line length 0003".into(),
5191                ));
5192            }
5193            4..=PKT_LINE_MAX_LEN => {
5194                let mut payload = vec![0; len - 4];
5195                reader.read_exact(&mut payload)?;
5196                payload
5197            }
5198            _ => {
5199                return Err(GitError::InvalidFormat(format!(
5200                    "pkt-line length exceeds {PKT_LINE_MAX_LEN}: {len}"
5201                )));
5202            }
5203        };
5204        trace_packet_read_payload(&payload);
5205        let trimmed = trim_trailing_lf(&payload);
5206        if trimmed == b"NAK" || trimmed.starts_with(b"ACK ") {
5207            acknowledgments.push(parse_upload_pack_acknowledgment(format, &payload)?);
5208            continue;
5209        }
5210        return Err(GitError::InvalidFormat(
5211            "upload-pack raw packfile response has non-ack pkt-line before packfile".into(),
5212        ));
5213    }
5214}
5215
5216pub fn read_upload_pack_shallow_info_section(
5217    format: ObjectFormat,
5218    reader: &mut impl Read,
5219) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
5220    let mut entries = Vec::new();
5221    loop {
5222        let Some(frame) = read_pkt_line_frame(reader)? else {
5223            return Err(GitError::InvalidFormat(
5224                "upload-pack shallow-info section ended before flush".into(),
5225            ));
5226        };
5227        match frame {
5228            PktLineFrame::Data(payload) => {
5229                entries.push(parse_fetch_shallow_info(format, &payload)?)
5230            }
5231            PktLineFrame::Flush => return Ok(entries),
5232            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5233                return Err(GitError::InvalidFormat(
5234                    "upload-pack shallow-info section contains a non-flush control packet".into(),
5235                ));
5236            }
5237        }
5238    }
5239}
5240
5241pub fn read_upload_pack_shallow_info_and_raw_packfile_response_header(
5242    format: ObjectFormat,
5243    reader: &mut impl Read,
5244) -> Result<(
5245    Vec<ProtocolV2FetchShallowInfo>,
5246    UploadPackRawPackfileResponseHeader,
5247)> {
5248    let shallow = read_upload_pack_shallow_info_section(format, reader)?;
5249    let raw = read_upload_pack_raw_packfile_response_header(format, reader)?;
5250    Ok((shallow, raw))
5251}
5252
5253pub fn write_upload_pack_raw_packfile_response(
5254    writer: &mut impl Write,
5255    response: &UploadPackRawPackfileResponse,
5256) -> Result<()> {
5257    if response.packfile.is_empty() {
5258        return Err(GitError::InvalidFormat(
5259            "upload-pack raw packfile response missing packfile".into(),
5260        ));
5261    }
5262    if !response.packfile.starts_with(b"PACK") {
5263        return Err(GitError::InvalidFormat(
5264            "upload-pack raw packfile response packfile must start with PACK".into(),
5265        ));
5266    }
5267    for acknowledgment in &response.acknowledgments {
5268        write_upload_pack_acknowledgment(writer, acknowledgment)?;
5269    }
5270    writer.write_all(&response.packfile)?;
5271    Ok(())
5272}
5273
5274/// Parse the smart-HTTP/SSH v0 *shallow-info* section that precedes the packfile
5275/// when the upload-pack request carried `shallow`/`deepen`/`deepen-since`/
5276/// `deepen-not` arguments.
5277///
5278/// The section is zero or more `shallow <oid>` / `unshallow <oid>` pkt-lines
5279/// terminated by a flush-pkt; git always emits it (even empty, as a bare flush)
5280/// when the request was a deepen request, and never emits it otherwise. Returns
5281/// the parsed entries and the number of bytes consumed (through the flush) so the
5282/// caller can continue parsing the trailing acknowledgments + packfile from
5283/// `&input[consumed..]` (see [`parse_upload_pack_shallow_info_and_raw_packfile_response`]).
5284pub fn parse_upload_pack_shallow_info_section(
5285    format: ObjectFormat,
5286    input: &[u8],
5287) -> Result<(Vec<ProtocolV2FetchShallowInfo>, usize)> {
5288    let mut entries = Vec::new();
5289    let mut offset = 0usize;
5290    loop {
5291        let (frame, consumed) = PktLineFrame::parse(&input[offset..])?;
5292        offset += consumed;
5293        match frame {
5294            PktLineFrame::Data(payload) => {
5295                entries.push(parse_fetch_shallow_info(format, &payload)?)
5296            }
5297            PktLineFrame::Flush => return Ok((entries, offset)),
5298            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5299                return Err(GitError::InvalidFormat(
5300                    "upload-pack shallow-info section contains a non-flush control packet".into(),
5301                ));
5302            }
5303        }
5304    }
5305}
5306
5307/// Parse a raw upload-pack response that begins with a *shallow-info* section,
5308/// i.e. the response to a deepen request.
5309///
5310/// This is [`parse_upload_pack_raw_packfile_response`] preceded by the
5311/// shallow-info section ([`parse_upload_pack_shallow_info_section`]): it returns
5312/// the `shallow`/`unshallow` entries the server reported alongside the parsed
5313/// acknowledgments + raw packfile. Use it only when the request carried a
5314/// `shallow`/`deepen`/`deepen-since`/`deepen-not` argument; for a plain (non-deepen)
5315/// request the response has no leading shallow-info section and
5316/// [`parse_upload_pack_raw_packfile_response`] must be used instead.
5317pub fn parse_upload_pack_shallow_info_and_raw_packfile_response(
5318    format: ObjectFormat,
5319    input: &[u8],
5320) -> Result<(
5321    Vec<ProtocolV2FetchShallowInfo>,
5322    UploadPackRawPackfileResponse,
5323)> {
5324    let (shallow, consumed) = parse_upload_pack_shallow_info_section(format, input)?;
5325    let response = parse_upload_pack_raw_packfile_response(format, &input[consumed..])?;
5326    Ok((shallow, response))
5327}
5328
5329/// Read a raw upload-pack response that begins with a *shallow-info* section from
5330/// `reader`, returning the `shallow`/`unshallow` entries and the parsed
5331/// acknowledgments + raw packfile.
5332///
5333/// The reader counterpart of
5334/// [`parse_upload_pack_shallow_info_and_raw_packfile_response`]; see it for when
5335/// this applies.
5336pub fn read_upload_pack_shallow_info_and_raw_packfile_response(
5337    format: ObjectFormat,
5338    reader: &mut impl Read,
5339) -> Result<(
5340    Vec<ProtocolV2FetchShallowInfo>,
5341    UploadPackRawPackfileResponse,
5342)> {
5343    let (shallow, header) =
5344        read_upload_pack_shallow_info_and_raw_packfile_response_header(format, reader)?;
5345    let mut packfile = header.pack_prefix;
5346    reader.read_to_end(&mut packfile)?;
5347    Ok((
5348        shallow,
5349        UploadPackRawPackfileResponse {
5350            acknowledgments: header.acknowledgments,
5351            packfile,
5352        },
5353    ))
5354}
5355
5356pub fn parse_receive_pack_request(
5357    format: ObjectFormat,
5358    frames: &[PktLineFrame],
5359) -> Result<ReceivePackRequest> {
5360    let mut request = ReceivePackRequest::default();
5361    let mut saw_command = false;
5362    let mut saw_flush = false;
5363    for (idx, frame) in frames.iter().enumerate() {
5364        match frame {
5365            PktLineFrame::Data(payload) if !saw_flush => {
5366                let payload = trim_trailing_lf(payload);
5367                if payload.is_empty() {
5368                    return Err(GitError::InvalidFormat(
5369                        "receive-pack request line is empty".into(),
5370                    ));
5371                }
5372                if let Some(shallow) = payload.strip_prefix(b"shallow ") {
5373                    if saw_command {
5374                        return Err(GitError::InvalidFormat(
5375                            "receive-pack request has shallow after commands".into(),
5376                        ));
5377                    }
5378                    let shallow = std::str::from_utf8(shallow)
5379                        .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
5380                    validate_protocol_v2_token("receive-pack shallow", shallow)?;
5381                    request.shallow.push(ObjectId::from_hex(format, shallow)?);
5382                    continue;
5383                }
5384
5385                let (command, capabilities) = match payload.iter().position(|byte| *byte == 0) {
5386                    Some(nul) if !saw_command => (
5387                        &payload[..nul],
5388                        Some(parse_capabilities(&payload[nul + 1..])?),
5389                    ),
5390                    Some(_) => {
5391                        return Err(GitError::InvalidFormat(
5392                            "receive-pack capabilities must appear on the first command".into(),
5393                        ));
5394                    }
5395                    None => (payload, None),
5396                };
5397                let command = parse_receive_pack_command(format, command)?;
5398                if let Some(capabilities) = capabilities {
5399                    request.capabilities = capabilities;
5400                }
5401                request.commands.push(command);
5402                saw_command = true;
5403            }
5404            PktLineFrame::Data(_) => {
5405                return Err(GitError::InvalidFormat(
5406                    "receive-pack request has data after flush".into(),
5407                ));
5408            }
5409            PktLineFrame::Flush => {
5410                saw_flush = true;
5411                if idx + 1 != frames.len() {
5412                    return Err(GitError::InvalidFormat(
5413                        "receive-pack request has frames after flush".into(),
5414                    ));
5415                }
5416            }
5417            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5418                return Err(GitError::InvalidFormat(
5419                    "receive-pack request contains a non-flush control packet".into(),
5420                ));
5421            }
5422        }
5423    }
5424    if !saw_flush {
5425        return Err(GitError::InvalidFormat(
5426            "receive-pack request missing flush".into(),
5427        ));
5428    }
5429    if !request.shallow.is_empty() && request.commands.is_empty() {
5430        return Err(GitError::InvalidFormat(
5431            "receive-pack request has shallow lines without commands".into(),
5432        ));
5433    }
5434    Ok(request)
5435}
5436
5437pub fn encode_receive_pack_request(request: &ReceivePackRequest) -> Result<Vec<PktLineFrame>> {
5438    if !request.shallow.is_empty() && request.commands.is_empty() {
5439        return Err(GitError::InvalidFormat(
5440            "receive-pack request has shallow lines without commands".into(),
5441        ));
5442    }
5443
5444    let mut frames = Vec::new();
5445    for oid in &request.shallow {
5446        frames.push(PktLineFrame::data(line_from_str(&format!(
5447            "shallow {oid}"
5448        )))?);
5449    }
5450    for (idx, command) in request.commands.iter().enumerate() {
5451        let mut payload = format_receive_pack_command(command)?;
5452        if idx == 0 && !request.capabilities.is_empty() {
5453            payload.push(0);
5454            payload.extend_from_slice(&encode_capabilities(&request.capabilities)?);
5455        }
5456        payload.push(b'\n');
5457        frames.push(PktLineFrame::data(payload)?);
5458    }
5459    frames.push(PktLineFrame::Flush);
5460    Ok(frames)
5461}
5462
5463pub fn read_receive_pack_request(
5464    format: ObjectFormat,
5465    reader: &mut impl Read,
5466) -> Result<ReceivePackRequest> {
5467    let frames = read_pkt_line_frames_until_flush(reader)?;
5468    parse_receive_pack_request(format, &frames)
5469}
5470
5471pub fn write_receive_pack_request(
5472    writer: &mut impl Write,
5473    request: &ReceivePackRequest,
5474) -> Result<()> {
5475    if !request.shallow.is_empty() && request.commands.is_empty() {
5476        return Err(GitError::InvalidFormat(
5477            "receive-pack request has shallow lines without commands".into(),
5478        ));
5479    }
5480
5481    for oid in &request.shallow {
5482        write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
5483    }
5484    for (idx, command) in request.commands.iter().enumerate() {
5485        let mut payload = format_receive_pack_command(command)?;
5486        if idx == 0 && !request.capabilities.is_empty() {
5487            payload.push(0);
5488            payload.extend_from_slice(&encode_capabilities(&request.capabilities)?);
5489        }
5490        payload.push(b'\n');
5491        write_pkt_line_payload(writer, &payload)?;
5492    }
5493    writer.write_all(b"0000")?;
5494    Ok(())
5495}
5496
5497pub fn parse_receive_pack_push_request(
5498    format: ObjectFormat,
5499    input: &[u8],
5500    has_push_options: bool,
5501) -> Result<ReceivePackPushRequest> {
5502    let (command_frames, consumed) = parse_pkt_line_frames_until_flush_from(input)?;
5503    let commands = parse_receive_pack_request(format, &command_frames)?;
5504    let mut offset = consumed;
5505    let push_options = if has_push_options {
5506        let (push_option_frames, consumed) =
5507            parse_pkt_line_frames_until_flush_from(&input[offset..])?;
5508        offset += consumed;
5509        Some(parse_receive_pack_push_options(&push_option_frames)?)
5510    } else {
5511        None
5512    };
5513    Ok(ReceivePackPushRequest {
5514        commands,
5515        push_options,
5516        packfile: input[offset..].to_vec(),
5517    })
5518}
5519
5520pub fn encode_receive_pack_push_request(request: &ReceivePackPushRequest) -> Result<Vec<u8>> {
5521    let mut out = Vec::new();
5522    write_receive_pack_request(&mut out, &request.commands)?;
5523    if let Some(push_options) = &request.push_options {
5524        write_receive_pack_push_options(&mut out, push_options)?;
5525    }
5526    out.extend_from_slice(&request.packfile);
5527    Ok(out)
5528}
5529
5530pub fn read_receive_pack_push_request(
5531    format: ObjectFormat,
5532    reader: &mut impl Read,
5533    has_push_options: bool,
5534) -> Result<ReceivePackPushRequest> {
5535    let header = read_receive_pack_push_request_header(format, reader, has_push_options)?;
5536    let mut packfile = Vec::new();
5537    reader.read_to_end(&mut packfile)?;
5538    Ok(ReceivePackPushRequest {
5539        commands: header.commands,
5540        push_options: header.push_options,
5541        packfile,
5542    })
5543}
5544
5545pub fn read_receive_pack_push_request_header(
5546    format: ObjectFormat,
5547    reader: &mut impl Read,
5548    has_push_options: bool,
5549) -> Result<ReceivePackPushRequestHeader> {
5550    let commands = read_receive_pack_request(format, reader)?;
5551    let push_options = if has_push_options {
5552        Some(read_receive_pack_push_options(reader)?)
5553    } else {
5554        None
5555    };
5556    Ok(ReceivePackPushRequestHeader {
5557        commands,
5558        push_options,
5559    })
5560}
5561
5562pub fn write_receive_pack_push_request(
5563    writer: &mut impl Write,
5564    request: &ReceivePackPushRequest,
5565) -> Result<()> {
5566    write_receive_pack_push_request_header(
5567        writer,
5568        &ReceivePackPushRequestHeader {
5569            commands: request.commands.clone(),
5570            push_options: request.push_options.clone(),
5571        },
5572    )?;
5573    writer.write_all(&request.packfile)?;
5574    Ok(())
5575}
5576
5577pub fn write_receive_pack_push_request_header(
5578    writer: &mut impl Write,
5579    header: &ReceivePackPushRequestHeader,
5580) -> Result<()> {
5581    write_receive_pack_request(writer, &header.commands)?;
5582    if let Some(push_options) = &header.push_options {
5583        write_receive_pack_push_options(writer, push_options)?;
5584    }
5585    Ok(())
5586}
5587
5588pub fn parse_receive_pack_features(capabilities: &[Capability]) -> Result<ReceivePackFeatures> {
5589    let mut features = ReceivePackFeatures::default();
5590    for capability in capabilities {
5591        match capability.name.as_str() {
5592            "report-status" => {
5593                reject_capability_value("receive-pack report-status", capability)?;
5594                if features.report_status {
5595                    return Err(GitError::InvalidFormat(
5596                        "receive-pack has duplicate report-status capability".into(),
5597                    ));
5598                }
5599                features.report_status = true;
5600            }
5601            "report-status-v2" => {
5602                reject_capability_value("receive-pack report-status-v2", capability)?;
5603                if features.report_status_v2 {
5604                    return Err(GitError::InvalidFormat(
5605                        "receive-pack has duplicate report-status-v2 capability".into(),
5606                    ));
5607                }
5608                features.report_status_v2 = true;
5609            }
5610            "delete-refs" => {
5611                reject_capability_value("receive-pack delete-refs", capability)?;
5612                if features.delete_refs {
5613                    return Err(GitError::InvalidFormat(
5614                        "receive-pack has duplicate delete-refs capability".into(),
5615                    ));
5616                }
5617                features.delete_refs = true;
5618            }
5619            "ofs-delta" => {
5620                reject_capability_value("receive-pack ofs-delta", capability)?;
5621                if features.ofs_delta {
5622                    return Err(GitError::InvalidFormat(
5623                        "receive-pack has duplicate ofs-delta capability".into(),
5624                    ));
5625                }
5626                features.ofs_delta = true;
5627            }
5628            "atomic" => {
5629                reject_capability_value("receive-pack atomic", capability)?;
5630                if features.atomic {
5631                    return Err(GitError::InvalidFormat(
5632                        "receive-pack has duplicate atomic capability".into(),
5633                    ));
5634                }
5635                features.atomic = true;
5636            }
5637            "push-options" => {
5638                reject_capability_value("receive-pack push-options", capability)?;
5639                if features.push_options {
5640                    return Err(GitError::InvalidFormat(
5641                        "receive-pack has duplicate push-options capability".into(),
5642                    ));
5643                }
5644                features.push_options = true;
5645            }
5646            "side-band-64k" => {
5647                reject_capability_value("receive-pack side-band-64k", capability)?;
5648                if features.side_band_64k {
5649                    return Err(GitError::InvalidFormat(
5650                        "receive-pack has duplicate side-band-64k capability".into(),
5651                    ));
5652                }
5653                features.side_band_64k = true;
5654            }
5655            "quiet" => {
5656                reject_capability_value("receive-pack quiet", capability)?;
5657                if features.quiet {
5658                    return Err(GitError::InvalidFormat(
5659                        "receive-pack has duplicate quiet capability".into(),
5660                    ));
5661                }
5662                features.quiet = true;
5663            }
5664            "no-thin" => {
5665                reject_capability_value("receive-pack no-thin", capability)?;
5666                if features.no_thin {
5667                    return Err(GitError::InvalidFormat(
5668                        "receive-pack has duplicate no-thin capability".into(),
5669                    ));
5670                }
5671                features.no_thin = true;
5672            }
5673            "agent" => {
5674                let Some(agent) = &capability.value else {
5675                    return Err(GitError::InvalidFormat(
5676                        "receive-pack agent capability is missing value".into(),
5677                    ));
5678                };
5679                if features.agent.is_some() {
5680                    return Err(GitError::InvalidFormat(
5681                        "receive-pack has duplicate agent capability".into(),
5682                    ));
5683                }
5684                validate_capability_field("receive-pack agent", agent)?;
5685                features.agent = Some(agent.clone());
5686            }
5687            "object-format" => {
5688                let Some(format) = &capability.value else {
5689                    return Err(GitError::InvalidFormat(
5690                        "receive-pack object-format capability is missing value".into(),
5691                    ));
5692                };
5693                if features.object_format.is_some() {
5694                    return Err(GitError::InvalidFormat(
5695                        "receive-pack has duplicate object-format capability".into(),
5696                    ));
5697                }
5698                validate_capability_field("receive-pack object-format", format)?;
5699                features.object_format = Some(format.parse()?);
5700            }
5701            _ => {
5702                encode_capabilities(std::slice::from_ref(capability))?;
5703                if features
5704                    .unknown
5705                    .iter()
5706                    .any(|known| known.name == capability.name)
5707                {
5708                    return Err(GitError::InvalidFormat(format!(
5709                        "receive-pack has duplicate {} capability",
5710                        capability.name
5711                    )));
5712                }
5713                features.unknown.push(capability.clone());
5714            }
5715        }
5716    }
5717    Ok(features)
5718}
5719
5720pub fn encode_receive_pack_features(features: &ReceivePackFeatures) -> Result<Vec<Capability>> {
5721    let mut capabilities = Vec::new();
5722    if features.report_status {
5723        capabilities.push(Capability {
5724            name: "report-status".into(),
5725            value: None,
5726        });
5727    }
5728    if features.report_status_v2 {
5729        capabilities.push(Capability {
5730            name: "report-status-v2".into(),
5731            value: None,
5732        });
5733    }
5734    if features.delete_refs {
5735        capabilities.push(Capability {
5736            name: "delete-refs".into(),
5737            value: None,
5738        });
5739    }
5740    if features.ofs_delta {
5741        capabilities.push(Capability {
5742            name: "ofs-delta".into(),
5743            value: None,
5744        });
5745    }
5746    if features.atomic {
5747        capabilities.push(Capability {
5748            name: "atomic".into(),
5749            value: None,
5750        });
5751    }
5752    if features.push_options {
5753        capabilities.push(Capability {
5754            name: "push-options".into(),
5755            value: None,
5756        });
5757    }
5758    if features.side_band_64k {
5759        capabilities.push(Capability {
5760            name: "side-band-64k".into(),
5761            value: None,
5762        });
5763    }
5764    if features.quiet {
5765        capabilities.push(Capability {
5766            name: "quiet".into(),
5767            value: None,
5768        });
5769    }
5770    if features.no_thin {
5771        capabilities.push(Capability {
5772            name: "no-thin".into(),
5773            value: None,
5774        });
5775    }
5776    if let Some(agent) = &features.agent {
5777        validate_capability_field("receive-pack agent", agent)?;
5778        capabilities.push(Capability {
5779            name: "agent".into(),
5780            value: Some(agent.clone()),
5781        });
5782    }
5783    if let Some(format) = features.object_format {
5784        capabilities.push(Capability {
5785            name: "object-format".into(),
5786            value: Some(format.name().into()),
5787        });
5788    }
5789    for capability in &features.unknown {
5790        if is_known_receive_pack_capability(&capability.name) {
5791            return Err(GitError::InvalidFormat(format!(
5792                "receive-pack unknown capability duplicates known capability {}",
5793                capability.name
5794            )));
5795        }
5796        encode_capabilities(std::slice::from_ref(capability))?;
5797        capabilities.push(capability.clone());
5798    }
5799    Ok(capabilities)
5800}
5801
5802pub fn validate_receive_pack_push_request_features(
5803    features: &ReceivePackFeatures,
5804    request: &ReceivePackPushRequest,
5805) -> Result<()> {
5806    for capability in &request.commands.capabilities {
5807        if matches!(
5808            capability.name.as_str(),
5809            "report-status"
5810                | "report-status-v2"
5811                | "delete-refs"
5812                | "ofs-delta"
5813                | "atomic"
5814                | "push-options"
5815                | "side-band-64k"
5816                | "quiet"
5817                | "no-thin"
5818        ) {
5819            reject_capability_value("receive-pack request capability", capability)?;
5820        }
5821        match capability.name.as_str() {
5822            "report-status" if !features.report_status => {
5823                return Err(GitError::InvalidFormat(
5824                    "receive-pack request uses report-status without advertised capability".into(),
5825                ));
5826            }
5827            "report-status-v2" if !features.report_status_v2 => {
5828                return Err(GitError::InvalidFormat(
5829                    "receive-pack request uses report-status-v2 without advertised capability"
5830                        .into(),
5831                ));
5832            }
5833            "delete-refs" if !features.delete_refs => {
5834                return Err(GitError::InvalidFormat(
5835                    "receive-pack request uses delete-refs without advertised capability".into(),
5836                ));
5837            }
5838            "ofs-delta" if !features.ofs_delta => {
5839                return Err(GitError::InvalidFormat(
5840                    "receive-pack request uses ofs-delta without advertised capability".into(),
5841                ));
5842            }
5843            "atomic" if !features.atomic => {
5844                return Err(GitError::InvalidFormat(
5845                    "receive-pack request uses atomic without advertised capability".into(),
5846                ));
5847            }
5848            "push-options" if !features.push_options => {
5849                return Err(GitError::InvalidFormat(
5850                    "receive-pack request uses push-options without advertised capability".into(),
5851                ));
5852            }
5853            "side-band-64k" if !features.side_band_64k => {
5854                return Err(GitError::InvalidFormat(
5855                    "receive-pack request uses side-band-64k without advertised capability".into(),
5856                ));
5857            }
5858            "quiet" if !features.quiet => {
5859                return Err(GitError::InvalidFormat(
5860                    "receive-pack request uses quiet without advertised capability".into(),
5861                ));
5862            }
5863            "no-thin" => {
5864                return Err(GitError::InvalidFormat(
5865                    "receive-pack request must not request no-thin".into(),
5866                ));
5867            }
5868            "agent" => {
5869                validate_capability_field(
5870                    "receive-pack request agent",
5871                    capability.value.as_deref().unwrap_or_default(),
5872                )?;
5873            }
5874            "object-format" => {
5875                let Some(value) = &capability.value else {
5876                    return Err(GitError::InvalidFormat(
5877                        "receive-pack request object-format capability is missing value".into(),
5878                    ));
5879                };
5880                let requested_format: ObjectFormat = value.parse()?;
5881                if features.object_format != Some(requested_format) {
5882                    return Err(GitError::InvalidFormat(
5883                        "receive-pack request object-format was not advertised".into(),
5884                    ));
5885                }
5886            }
5887            name if is_known_receive_pack_capability(name) => {}
5888            _ => {
5889                if !features
5890                    .unknown
5891                    .iter()
5892                    .any(|advertised| advertised.name == capability.name)
5893                {
5894                    return Err(GitError::InvalidFormat(format!(
5895                        "receive-pack request uses unadvertised capability {}",
5896                        capability.name
5897                    )));
5898                }
5899            }
5900        }
5901    }
5902
5903    let requested_push_options = request
5904        .commands
5905        .capabilities
5906        .iter()
5907        .any(|capability| capability.name == "push-options");
5908    match (requested_push_options, &request.push_options) {
5909        (true, Some(_)) => {}
5910        (true, None) => {
5911            return Err(GitError::InvalidFormat(
5912                "receive-pack request uses push-options without push-options section".into(),
5913            ));
5914        }
5915        (false, Some(_)) => {
5916            return Err(GitError::InvalidFormat(
5917                "receive-pack request has push-options section without requested capability".into(),
5918            ));
5919        }
5920        (false, None) => {}
5921    }
5922
5923    let has_delete = request
5924        .commands
5925        .commands
5926        .iter()
5927        .any(is_receive_pack_delete_command);
5928    if has_delete && !features.delete_refs {
5929        return Err(GitError::InvalidFormat(
5930            "receive-pack request deletes refs without advertised delete-refs capability".into(),
5931        ));
5932    }
5933
5934    let has_update_or_create = request
5935        .commands
5936        .commands
5937        .iter()
5938        .any(|command| !is_receive_pack_delete_command(command));
5939    if !has_update_or_create && !request.packfile.is_empty() {
5940        return Err(GitError::InvalidFormat(
5941            "receive-pack delete-only request must not include packfile".into(),
5942        ));
5943    }
5944    Ok(())
5945}
5946
5947pub fn apply_receive_pack_push_request<R, I, C, U, D>(
5948    features: &ReceivePackFeatures,
5949    request: &ReceivePackPushRequest,
5950    mut read_ref: R,
5951    mut install_pack: I,
5952    mut contains_object: C,
5953    mut apply_updates: U,
5954    mut delete_ref: D,
5955) -> Result<ReceivePackReportStatus>
5956where
5957    R: FnMut(&str) -> Result<Option<ObjectId>>,
5958    I: FnMut(&[u8]) -> Result<()>,
5959    C: FnMut(&ObjectId) -> Result<bool>,
5960    U: FnMut(&[ReceivePackCommand]) -> Result<()>,
5961    D: FnMut(&ReceivePackCommand) -> Result<()>,
5962{
5963    validate_receive_pack_push_request_features(features, request)?;
5964
5965    for command in request
5966        .commands
5967        .commands
5968        .iter()
5969        .filter(|command| is_receive_pack_delete_command(command))
5970    {
5971        if !command.old_id.is_null() && read_ref(&command.name)? != Some(command.old_id.clone()) {
5972            return Err(GitError::Transaction(format!(
5973                "expected ref {} to match",
5974                command.name
5975            )));
5976        }
5977    }
5978
5979    let updates = request
5980        .commands
5981        .commands
5982        .iter()
5983        .filter(|command| !is_receive_pack_delete_command(command))
5984        .cloned()
5985        .collect::<Vec<_>>();
5986    if !updates.is_empty() {
5987        if !request.packfile.is_empty() {
5988            install_pack(&request.packfile)?;
5989        }
5990        for command in &updates {
5991            if !contains_object(&command.new_id)? {
5992                return Err(GitError::InvalidObject(format!(
5993                    "receive-pack packfile did not provide {}",
5994                    command.new_id
5995                )));
5996            }
5997        }
5998        apply_updates(&updates)?;
5999    }
6000
6001    for command in request
6002        .commands
6003        .commands
6004        .iter()
6005        .filter(|command| is_receive_pack_delete_command(command))
6006    {
6007        delete_ref(command)?;
6008    }
6009
6010    Ok(ReceivePackReportStatus {
6011        unpack: ReceivePackUnpackStatus::Ok,
6012        commands: request
6013            .commands
6014            .commands
6015            .iter()
6016            .map(|command| ReceivePackCommandStatus::Ok {
6017                name: command.name.clone(),
6018            })
6019            .collect(),
6020    })
6021}
6022
6023pub fn parse_receive_pack_report_status(
6024    frames: &[PktLineFrame],
6025) -> Result<ReceivePackReportStatus> {
6026    let Some((first, rest)) = frames.split_first() else {
6027        return Err(GitError::InvalidFormat(
6028            "receive-pack report-status is empty".into(),
6029        ));
6030    };
6031    let PktLineFrame::Data(payload) = first else {
6032        return Err(GitError::InvalidFormat(
6033            "receive-pack report-status must start with unpack status".into(),
6034        ));
6035    };
6036    let unpack = parse_receive_pack_unpack_status(payload)?;
6037
6038    let mut commands = Vec::new();
6039    let mut saw_flush = false;
6040    for (idx, frame) in rest.iter().enumerate() {
6041        match frame {
6042            PktLineFrame::Data(payload) if !saw_flush => {
6043                commands.push(parse_receive_pack_command_status(payload)?);
6044            }
6045            PktLineFrame::Data(_) => {
6046                return Err(GitError::InvalidFormat(
6047                    "receive-pack report-status has data after flush".into(),
6048                ));
6049            }
6050            PktLineFrame::Flush => {
6051                saw_flush = true;
6052                if idx + 1 != rest.len() {
6053                    return Err(GitError::InvalidFormat(
6054                        "receive-pack report-status has frames after flush".into(),
6055                    ));
6056                }
6057            }
6058            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
6059                return Err(GitError::InvalidFormat(
6060                    "receive-pack report-status contains a non-flush control packet".into(),
6061                ));
6062            }
6063        }
6064    }
6065    if !saw_flush {
6066        return Err(GitError::InvalidFormat(
6067            "receive-pack report-status missing flush".into(),
6068        ));
6069    }
6070    Ok(ReceivePackReportStatus { unpack, commands })
6071}
6072
6073pub fn encode_receive_pack_report_status(
6074    report: &ReceivePackReportStatus,
6075) -> Result<Vec<PktLineFrame>> {
6076    let mut frames = Vec::new();
6077    frames.push(PktLineFrame::data(line_from_str(
6078        &format_receive_pack_unpack_status(&report.unpack)?,
6079    ))?);
6080    for command in &report.commands {
6081        frames.push(PktLineFrame::data(line_from_str(
6082            &format_receive_pack_command_status(command)?,
6083        ))?);
6084    }
6085    frames.push(PktLineFrame::Flush);
6086    Ok(frames)
6087}
6088
6089pub fn read_receive_pack_report_status(reader: &mut impl Read) -> Result<ReceivePackReportStatus> {
6090    let frames = read_pkt_line_frames_until_flush(reader)?;
6091    parse_receive_pack_report_status(&frames)
6092}
6093
6094pub fn write_receive_pack_report_status(
6095    writer: &mut impl Write,
6096    report: &ReceivePackReportStatus,
6097) -> Result<()> {
6098    write_pkt_line_payload(
6099        writer,
6100        &line_from_str(&format_receive_pack_unpack_status(&report.unpack)?),
6101    )?;
6102    for command in &report.commands {
6103        write_pkt_line_payload(
6104            writer,
6105            &line_from_str(&format_receive_pack_command_status(command)?),
6106        )?;
6107    }
6108    writer.write_all(b"0000")?;
6109    Ok(())
6110}
6111
6112pub fn parse_receive_pack_report_status_v2(
6113    format: ObjectFormat,
6114    frames: &[PktLineFrame],
6115) -> Result<ReceivePackReportStatusV2> {
6116    let Some((first, rest)) = frames.split_first() else {
6117        return Err(GitError::InvalidFormat(
6118            "receive-pack report-status-v2 is empty".into(),
6119        ));
6120    };
6121    let PktLineFrame::Data(payload) = first else {
6122        return Err(GitError::InvalidFormat(
6123            "receive-pack report-status-v2 must start with unpack status".into(),
6124        ));
6125    };
6126    let unpack = parse_receive_pack_unpack_status(payload)?;
6127
6128    let mut commands = Vec::new();
6129    let mut saw_flush = false;
6130    for (idx, frame) in rest.iter().enumerate() {
6131        match frame {
6132            PktLineFrame::Data(payload) if !saw_flush => {
6133                let text =
6134                    parse_protocol_v2_line_text("receive-pack report-status-v2 line", payload)?;
6135                if text.starts_with("option ") {
6136                    let Some(ReceivePackCommandStatusV2::Ok { options, .. }) = commands.last_mut()
6137                    else {
6138                        return Err(GitError::InvalidFormat(
6139                            "receive-pack report-status-v2 option without ok status".into(),
6140                        ));
6141                    };
6142                    parse_receive_pack_report_status_v2_option(format, text, options)?;
6143                } else {
6144                    commands.push(parse_receive_pack_command_status_v2(text)?);
6145                }
6146            }
6147            PktLineFrame::Data(_) => {
6148                return Err(GitError::InvalidFormat(
6149                    "receive-pack report-status-v2 has data after flush".into(),
6150                ));
6151            }
6152            PktLineFrame::Flush => {
6153                saw_flush = true;
6154                if idx + 1 != rest.len() {
6155                    return Err(GitError::InvalidFormat(
6156                        "receive-pack report-status-v2 has frames after flush".into(),
6157                    ));
6158                }
6159            }
6160            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
6161                return Err(GitError::InvalidFormat(
6162                    "receive-pack report-status-v2 contains a non-flush control packet".into(),
6163                ));
6164            }
6165        }
6166    }
6167    if !saw_flush {
6168        return Err(GitError::InvalidFormat(
6169            "receive-pack report-status-v2 missing flush".into(),
6170        ));
6171    }
6172    Ok(ReceivePackReportStatusV2 { unpack, commands })
6173}
6174
6175pub fn encode_receive_pack_report_status_v2(
6176    report: &ReceivePackReportStatusV2,
6177) -> Result<Vec<PktLineFrame>> {
6178    let mut frames = Vec::new();
6179    frames.push(PktLineFrame::data(line_from_str(
6180        &format_receive_pack_unpack_status(&report.unpack)?,
6181    ))?);
6182    for command in &report.commands {
6183        frames.push(PktLineFrame::data(line_from_str(
6184            &format_receive_pack_command_status_v2(command)?,
6185        ))?);
6186        if let ReceivePackCommandStatusV2::Ok { options, .. } = command {
6187            for option in format_receive_pack_report_status_v2_options(options)? {
6188                frames.push(PktLineFrame::data(line_from_str(&option))?);
6189            }
6190        }
6191    }
6192    frames.push(PktLineFrame::Flush);
6193    Ok(frames)
6194}
6195
6196pub fn read_receive_pack_report_status_v2(
6197    format: ObjectFormat,
6198    reader: &mut impl Read,
6199) -> Result<ReceivePackReportStatusV2> {
6200    let frames = read_pkt_line_frames_until_flush(reader)?;
6201    parse_receive_pack_report_status_v2(format, &frames)
6202}
6203
6204pub fn write_receive_pack_report_status_v2(
6205    writer: &mut impl Write,
6206    report: &ReceivePackReportStatusV2,
6207) -> Result<()> {
6208    write_pkt_line_payload(
6209        writer,
6210        &line_from_str(&format_receive_pack_unpack_status(&report.unpack)?),
6211    )?;
6212    for command in &report.commands {
6213        write_pkt_line_payload(
6214            writer,
6215            &line_from_str(&format_receive_pack_command_status_v2(command)?),
6216        )?;
6217        if let ReceivePackCommandStatusV2::Ok { options, .. } = command {
6218            for option in format_receive_pack_report_status_v2_options(options)? {
6219                write_pkt_line_payload(writer, &line_from_str(&option))?;
6220            }
6221        }
6222    }
6223    writer.write_all(b"0000")?;
6224    Ok(())
6225}
6226
6227pub fn parse_receive_pack_push_options(frames: &[PktLineFrame]) -> Result<Vec<String>> {
6228    let mut options = Vec::new();
6229    let mut saw_flush = false;
6230    for (idx, frame) in frames.iter().enumerate() {
6231        match frame {
6232            PktLineFrame::Data(payload) if !saw_flush => {
6233                let option = trim_trailing_lf(payload);
6234                validate_receive_pack_push_option(option)?;
6235                options.push(
6236                    std::str::from_utf8(option)
6237                        .map_err(|err| GitError::InvalidFormat(err.to_string()))?
6238                        .to_string(),
6239                );
6240            }
6241            PktLineFrame::Data(_) => {
6242                return Err(GitError::InvalidFormat(
6243                    "receive-pack push-options has data after flush".into(),
6244                ));
6245            }
6246            PktLineFrame::Flush => {
6247                saw_flush = true;
6248                if idx + 1 != frames.len() {
6249                    return Err(GitError::InvalidFormat(
6250                        "receive-pack push-options has frames after flush".into(),
6251                    ));
6252                }
6253            }
6254            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
6255                return Err(GitError::InvalidFormat(
6256                    "receive-pack push-options contains a non-flush control packet".into(),
6257                ));
6258            }
6259        }
6260    }
6261    if !saw_flush {
6262        return Err(GitError::InvalidFormat(
6263            "receive-pack push-options missing flush".into(),
6264        ));
6265    }
6266    Ok(options)
6267}
6268
6269pub fn encode_receive_pack_push_options(options: &[String]) -> Result<Vec<PktLineFrame>> {
6270    let mut frames = Vec::new();
6271    for option in options {
6272        validate_receive_pack_push_option(option.as_bytes())?;
6273        let mut payload = option.as_bytes().to_vec();
6274        payload.push(b'\n');
6275        frames.push(PktLineFrame::data(payload)?);
6276    }
6277    frames.push(PktLineFrame::Flush);
6278    Ok(frames)
6279}
6280
6281pub fn read_receive_pack_push_options(reader: &mut impl Read) -> Result<Vec<String>> {
6282    let frames = read_pkt_line_frames_until_flush(reader)?;
6283    parse_receive_pack_push_options(&frames)
6284}
6285
6286pub fn write_receive_pack_push_options(writer: &mut impl Write, options: &[String]) -> Result<()> {
6287    for option in options {
6288        validate_receive_pack_push_option(option.as_bytes())?;
6289        let mut payload = option.as_bytes().to_vec();
6290        payload.push(b'\n');
6291        write_pkt_line_payload(writer, &payload)?;
6292    }
6293    writer.write_all(b"0000")?;
6294    Ok(())
6295}
6296
6297fn parse_receive_pack_command(format: ObjectFormat, payload: &[u8]) -> Result<ReceivePackCommand> {
6298    let text =
6299        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6300    let mut fields = text.split(' ');
6301    let old_id = fields
6302        .next()
6303        .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing old id".into()))?;
6304    let new_id = fields
6305        .next()
6306        .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing new id".into()))?;
6307    let name = fields
6308        .next()
6309        .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing ref name".into()))?;
6310    if fields.next().is_some() {
6311        return Err(GitError::InvalidFormat(
6312            "receive-pack command has too many fields".into(),
6313        ));
6314    }
6315    validate_protocol_v2_token("receive-pack old id", old_id)?;
6316    validate_protocol_v2_token("receive-pack new id", new_id)?;
6317    validate_protocol_v2_token("receive-pack ref name", name)?;
6318    Ok(ReceivePackCommand {
6319        old_id: ObjectId::from_hex(format, old_id)?,
6320        new_id: ObjectId::from_hex(format, new_id)?,
6321        name: name.to_string(),
6322    })
6323}
6324
6325fn format_receive_pack_command(command: &ReceivePackCommand) -> Result<Vec<u8>> {
6326    if command.old_id.format() != command.new_id.format() {
6327        return Err(GitError::InvalidObjectId(
6328            "receive-pack command object formats do not match".into(),
6329        ));
6330    }
6331    validate_protocol_v2_token("receive-pack ref name", &command.name)?;
6332    Ok(format!("{} {} {}", command.old_id, command.new_id, command.name).into_bytes())
6333}
6334
6335fn set_upload_pack_flag(value: &mut bool, capability: &Capability) -> Result<()> {
6336    reject_capability_value("upload-pack capability", capability)?;
6337    if *value {
6338        return Err(GitError::InvalidFormat(format!(
6339            "upload-pack has duplicate {} capability",
6340            capability.name
6341        )));
6342    }
6343    *value = true;
6344    Ok(())
6345}
6346
6347fn push_upload_pack_flag(capabilities: &mut Vec<Capability>, name: &str, enabled: bool) {
6348    if enabled {
6349        capabilities.push(Capability {
6350            name: name.into(),
6351            value: None,
6352        });
6353    }
6354}
6355
6356fn is_known_upload_pack_capability(name: &str) -> bool {
6357    matches!(
6358        name,
6359        "multi_ack"
6360            | "multi_ack_detailed"
6361            | "no-done"
6362            | "thin-pack"
6363            | "side-band"
6364            | "side-band-64k"
6365            | "ofs-delta"
6366            | "shallow"
6367            | "deepen-since"
6368            | "deepen-not"
6369            | "include-tag"
6370            | "no-progress"
6371            | "allow-tip-sha1-in-want"
6372            | "allow-reachable-sha1-in-want"
6373            | "filter"
6374            | "agent"
6375            | "object-format"
6376            | "symref"
6377    )
6378}
6379
6380fn is_upload_pack_flag_capability(name: &str) -> bool {
6381    matches!(
6382        name,
6383        "multi_ack"
6384            | "multi_ack_detailed"
6385            | "no-done"
6386            | "thin-pack"
6387            | "side-band"
6388            | "side-band-64k"
6389            | "ofs-delta"
6390            | "shallow"
6391            | "deepen-since"
6392            | "deepen-not"
6393            | "include-tag"
6394            | "no-progress"
6395            | "allow-tip-sha1-in-want"
6396            | "allow-reachable-sha1-in-want"
6397            | "filter"
6398    )
6399}
6400
6401fn reject_capability_value(label: &str, capability: &Capability) -> Result<()> {
6402    if capability.value.is_some() {
6403        return Err(GitError::InvalidFormat(format!(
6404            "{label} must not have value"
6405        )));
6406    }
6407    Ok(())
6408}
6409
6410fn is_known_receive_pack_capability(name: &str) -> bool {
6411    matches!(
6412        name,
6413        "report-status"
6414            | "report-status-v2"
6415            | "delete-refs"
6416            | "ofs-delta"
6417            | "atomic"
6418            | "push-options"
6419            | "side-band-64k"
6420            | "quiet"
6421            | "no-thin"
6422            | "agent"
6423            | "object-format"
6424    )
6425}
6426
6427fn is_receive_pack_delete_command(command: &ReceivePackCommand) -> bool {
6428    command.new_id.is_null()
6429}
6430
6431fn parse_receive_pack_unpack_status(payload: &[u8]) -> Result<ReceivePackUnpackStatus> {
6432    let text = parse_protocol_v2_line_text("receive-pack unpack status", payload)?;
6433    if text == "unpack ok" {
6434        return Ok(ReceivePackUnpackStatus::Ok);
6435    }
6436    let Some(message) = text.strip_prefix("unpack ") else {
6437        return Err(GitError::InvalidFormat(format!(
6438            "unsupported receive-pack unpack status {text}"
6439        )));
6440    };
6441    validate_receive_pack_status_message("receive-pack unpack error", message)?;
6442    Ok(ReceivePackUnpackStatus::Error(message.to_string()))
6443}
6444
6445fn format_receive_pack_unpack_status(status: &ReceivePackUnpackStatus) -> Result<String> {
6446    match status {
6447        ReceivePackUnpackStatus::Ok => Ok("unpack ok".into()),
6448        ReceivePackUnpackStatus::Error(message) => {
6449            validate_receive_pack_status_message("receive-pack unpack error", message)?;
6450            Ok(format!("unpack {message}"))
6451        }
6452    }
6453}
6454
6455fn parse_receive_pack_command_status(payload: &[u8]) -> Result<ReceivePackCommandStatus> {
6456    let text = parse_protocol_v2_line_text("receive-pack command status", payload)?;
6457    if let Some(name) = text.strip_prefix("ok ") {
6458        validate_protocol_v2_token("receive-pack status ref name", name)?;
6459        return Ok(ReceivePackCommandStatus::Ok {
6460            name: name.to_string(),
6461        });
6462    }
6463    let Some(rest) = text.strip_prefix("ng ") else {
6464        return Err(GitError::InvalidFormat(format!(
6465            "unsupported receive-pack command status {text}"
6466        )));
6467    };
6468    let (name, message) = rest
6469        .split_once(' ')
6470        .ok_or_else(|| GitError::InvalidFormat("receive-pack ng status missing message".into()))?;
6471    validate_protocol_v2_token("receive-pack status ref name", name)?;
6472    validate_receive_pack_status_message("receive-pack ng status message", message)?;
6473    Ok(ReceivePackCommandStatus::Ng {
6474        name: name.to_string(),
6475        message: message.to_string(),
6476    })
6477}
6478
6479fn format_receive_pack_command_status(status: &ReceivePackCommandStatus) -> Result<String> {
6480    match status {
6481        ReceivePackCommandStatus::Ok { name } => {
6482            validate_protocol_v2_token("receive-pack status ref name", name)?;
6483            Ok(format!("ok {name}"))
6484        }
6485        ReceivePackCommandStatus::Ng { name, message } => {
6486            validate_protocol_v2_token("receive-pack status ref name", name)?;
6487            validate_receive_pack_status_message("receive-pack ng status message", message)?;
6488            Ok(format!("ng {name} {message}"))
6489        }
6490    }
6491}
6492
6493fn parse_receive_pack_command_status_v2(text: &str) -> Result<ReceivePackCommandStatusV2> {
6494    if let Some(name) = text.strip_prefix("ok ") {
6495        validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6496        return Ok(ReceivePackCommandStatusV2::Ok {
6497            name: name.to_string(),
6498            options: ReceivePackCommandStatusV2Options::default(),
6499        });
6500    }
6501    let Some(rest) = text.strip_prefix("ng ") else {
6502        return Err(GitError::InvalidFormat(format!(
6503            "unsupported receive-pack report-status-v2 line {text}"
6504        )));
6505    };
6506    let (name, message) = rest.split_once(' ').ok_or_else(|| {
6507        GitError::InvalidFormat("receive-pack status-v2 ng status missing message".into())
6508    })?;
6509    validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6510    validate_receive_pack_status_message("receive-pack status-v2 ng message", message)?;
6511    Ok(ReceivePackCommandStatusV2::Ng {
6512        name: name.to_string(),
6513        message: message.to_string(),
6514    })
6515}
6516
6517fn format_receive_pack_command_status_v2(status: &ReceivePackCommandStatusV2) -> Result<String> {
6518    match status {
6519        ReceivePackCommandStatusV2::Ok { name, .. } => {
6520            validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6521            Ok(format!("ok {name}"))
6522        }
6523        ReceivePackCommandStatusV2::Ng { name, message } => {
6524            validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6525            validate_receive_pack_status_message("receive-pack status-v2 ng message", message)?;
6526            Ok(format!("ng {name} {message}"))
6527        }
6528    }
6529}
6530
6531fn parse_receive_pack_report_status_v2_option(
6532    format: ObjectFormat,
6533    text: &str,
6534    options: &mut ReceivePackCommandStatusV2Options,
6535) -> Result<()> {
6536    if let Some(refname) = text.strip_prefix("option refname ") {
6537        if options.refname.is_some() {
6538            return Err(GitError::InvalidFormat(
6539                "receive-pack report-status-v2 has duplicate refname option".into(),
6540            ));
6541        }
6542        validate_protocol_v2_token("receive-pack status-v2 option refname", refname)?;
6543        options.refname = Some(refname.to_string());
6544    } else if let Some(old_oid) = text.strip_prefix("option old-oid ") {
6545        if options.old_oid.is_some() {
6546            return Err(GitError::InvalidFormat(
6547                "receive-pack report-status-v2 has duplicate old-oid option".into(),
6548            ));
6549        }
6550        validate_protocol_v2_token("receive-pack status-v2 option old-oid", old_oid)?;
6551        options.old_oid = Some(ObjectId::from_hex(format, old_oid)?);
6552    } else if let Some(new_oid) = text.strip_prefix("option new-oid ") {
6553        if options.new_oid.is_some() {
6554            return Err(GitError::InvalidFormat(
6555                "receive-pack report-status-v2 has duplicate new-oid option".into(),
6556            ));
6557        }
6558        validate_protocol_v2_token("receive-pack status-v2 option new-oid", new_oid)?;
6559        options.new_oid = Some(ObjectId::from_hex(format, new_oid)?);
6560    } else if text == "option forced-update" {
6561        if options.forced_update {
6562            return Err(GitError::InvalidFormat(
6563                "receive-pack report-status-v2 has duplicate forced-update option".into(),
6564            ));
6565        }
6566        options.forced_update = true;
6567    } else {
6568        return Err(GitError::InvalidFormat(format!(
6569            "unsupported receive-pack report-status-v2 option {text}"
6570        )));
6571    }
6572    Ok(())
6573}
6574
6575fn format_receive_pack_report_status_v2_options(
6576    options: &ReceivePackCommandStatusV2Options,
6577) -> Result<Vec<String>> {
6578    let mut out = Vec::new();
6579    if let Some(refname) = &options.refname {
6580        validate_protocol_v2_token("receive-pack status-v2 option refname", refname)?;
6581        out.push(format!("option refname {refname}"));
6582    }
6583    if let Some(old_oid) = &options.old_oid {
6584        out.push(format!("option old-oid {old_oid}"));
6585    }
6586    if let Some(new_oid) = &options.new_oid {
6587        out.push(format!("option new-oid {new_oid}"));
6588    }
6589    if options.forced_update {
6590        out.push("option forced-update".into());
6591    }
6592    Ok(out)
6593}
6594
6595fn validate_receive_pack_status_message(label: &str, message: &str) -> Result<()> {
6596    if message.is_empty() {
6597        return Err(GitError::InvalidFormat(format!("{label} is empty")));
6598    }
6599    if message
6600        .bytes()
6601        .any(|byte| matches!(byte, b'\n' | b'\r' | 0))
6602    {
6603        return Err(GitError::InvalidFormat(format!(
6604            "{label} contains a delimiter byte"
6605        )));
6606    }
6607    Ok(())
6608}
6609
6610fn validate_receive_pack_push_option(option: &[u8]) -> Result<()> {
6611    if option.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
6612        return Err(GitError::InvalidFormat(
6613            "receive-pack push-option contains a delimiter byte".into(),
6614        ));
6615    }
6616    Ok(())
6617}
6618
6619fn validate_protocol_error_message(message: &str) -> Result<()> {
6620    if message.is_empty() {
6621        return Err(GitError::InvalidFormat(
6622            "protocol error message is empty".into(),
6623        ));
6624    }
6625    if message
6626        .bytes()
6627        .any(|byte| matches!(byte, b'\n' | b'\r' | 0))
6628    {
6629        return Err(GitError::InvalidFormat(
6630            "protocol error message contains a delimiter byte".into(),
6631        ));
6632    }
6633    Ok(())
6634}
6635
6636fn parse_capability_token(token: &str) -> Result<Capability> {
6637    if token.is_empty() {
6638        return Err(GitError::InvalidFormat("empty capability token".into()));
6639    }
6640    let (name, value) = token
6641        .split_once('=')
6642        .map_or((token, None), |(name, value)| (name, Some(value)));
6643    validate_capability_field("capability name", name)?;
6644    if let Some(value) = value {
6645        validate_capability_field("capability value", value)?;
6646    }
6647    Ok(Capability {
6648        name: name.to_string(),
6649        value: value.map(str::to_string),
6650    })
6651}
6652
6653fn parse_protocol_v2_capability_line(payload: &[u8]) -> Result<Capability> {
6654    let payload = trim_trailing_lf(payload);
6655    if payload.is_empty() {
6656        return Err(GitError::InvalidFormat(
6657            "empty protocol v2 capability line".into(),
6658        ));
6659    }
6660    let text =
6661        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6662    let (name, value) = text
6663        .split_once('=')
6664        .map_or((text, None), |(name, value)| (name, Some(value)));
6665    validate_capability_name(name)?;
6666    if let Some(value) = value {
6667        validate_protocol_v2_capability_value(value)?;
6668    }
6669    Ok(Capability {
6670        name: name.to_string(),
6671        value: value.map(str::to_string),
6672    })
6673}
6674
6675fn parse_protocol_v2_command_line(payload: &[u8]) -> Result<String> {
6676    let payload = trim_trailing_lf(payload);
6677    let text =
6678        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6679    let Some(command) = text.strip_prefix("command=") else {
6680        return Err(GitError::InvalidFormat(
6681            "protocol v2 command request missing command prefix".into(),
6682        ));
6683    };
6684    validate_capability_name(command)?;
6685    Ok(command.to_string())
6686}
6687
6688fn parse_protocol_v2_ls_refs_line(
6689    format: ObjectFormat,
6690    payload: &[u8],
6691) -> Result<ProtocolV2LsRefsRecord> {
6692    let payload = trim_trailing_lf(payload);
6693    if payload.is_empty() {
6694        return Err(GitError::InvalidFormat(
6695            "ls-refs response line is empty".into(),
6696        ));
6697    }
6698    let text =
6699        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6700    let mut fields = text.split(' ');
6701    let first = fields
6702        .next()
6703        .ok_or_else(|| GitError::InvalidFormat("ls-refs response line is empty".into()))?;
6704    if first == "unborn" {
6705        let name = fields
6706            .next()
6707            .ok_or_else(|| GitError::InvalidFormat("ls-refs unborn line is missing name".into()))?;
6708        validate_protocol_v2_token("ls-refs ref name", name)?;
6709        let (symref_target, attributes) = parse_protocol_v2_ls_refs_attributes(format, fields)?;
6710        return Ok(ProtocolV2LsRefsRecord::Unborn {
6711            name: name.to_string(),
6712            symref_target,
6713            attributes,
6714        });
6715    }
6716
6717    let oid = ObjectId::from_hex(format, first)?;
6718    let name = fields
6719        .next()
6720        .ok_or_else(|| GitError::InvalidFormat("ls-refs ref line is missing name".into()))?;
6721    validate_protocol_v2_token("ls-refs ref name", name)?;
6722    let (peeled, symref_target, attributes) =
6723        parse_protocol_v2_ls_refs_ref_attributes(format, fields)?;
6724    Ok(ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
6725        oid,
6726        name: name.to_string(),
6727        peeled,
6728        symref_target,
6729        attributes,
6730    }))
6731}
6732
6733fn parse_protocol_v2_ls_refs_ref_attributes<'a>(
6734    format: ObjectFormat,
6735    fields: impl Iterator<Item = &'a str>,
6736) -> Result<(Option<ObjectId>, Option<String>, Vec<String>)> {
6737    let mut peeled = None;
6738    let (symref_target, attributes) =
6739        parse_protocol_v2_ls_refs_attributes_with(format, fields, |attr| {
6740            if let Some(value) = attr.strip_prefix("peeled:") {
6741                if peeled.is_some() {
6742                    return Err(GitError::InvalidFormat(
6743                        "ls-refs response has duplicate peeled attribute".into(),
6744                    ));
6745                }
6746                peeled = Some(ObjectId::from_hex(format, value)?);
6747                return Ok(true);
6748            }
6749            Ok(false)
6750        })?;
6751    Ok((peeled, symref_target, attributes))
6752}
6753
6754fn parse_protocol_v2_ls_refs_attributes<'a>(
6755    format: ObjectFormat,
6756    fields: impl Iterator<Item = &'a str>,
6757) -> Result<(Option<String>, Vec<String>)> {
6758    parse_protocol_v2_ls_refs_attributes_with(format, fields, |attr| {
6759        if attr.starts_with("peeled:") {
6760            return Err(GitError::InvalidFormat(
6761                "ls-refs unborn line has peeled attribute".into(),
6762            ));
6763        }
6764        Ok(false)
6765    })
6766}
6767
6768fn parse_protocol_v2_ls_refs_attributes_with<'a, F>(
6769    _format: ObjectFormat,
6770    fields: impl Iterator<Item = &'a str>,
6771    mut handle_known: F,
6772) -> Result<(Option<String>, Vec<String>)>
6773where
6774    F: FnMut(&str) -> Result<bool>,
6775{
6776    let mut symref_target = None;
6777    let mut attributes = Vec::new();
6778    for attr in fields {
6779        validate_protocol_v2_token("ls-refs attribute", attr)?;
6780        if let Some(value) = attr.strip_prefix("symref-target:") {
6781            if symref_target.is_some() {
6782                return Err(GitError::InvalidFormat(
6783                    "ls-refs response has duplicate symref-target attribute".into(),
6784                ));
6785            }
6786            validate_protocol_v2_token("ls-refs symref-target", value)?;
6787            symref_target = Some(value.to_string());
6788        } else if !handle_known(attr)? {
6789            attributes.push(attr.to_string());
6790        }
6791    }
6792    Ok((symref_target, attributes))
6793}
6794
6795fn format_protocol_v2_ls_refs_record(record: &ProtocolV2LsRefsRecord) -> Result<String> {
6796    let mut out = String::new();
6797    match record {
6798        ProtocolV2LsRefsRecord::Ref(reference) => {
6799            validate_protocol_v2_token("ls-refs ref name", &reference.name)?;
6800            out.push_str(&reference.oid.to_string());
6801            out.push(' ');
6802            out.push_str(&reference.name);
6803            if let Some(peeled) = &reference.peeled {
6804                if peeled.format() != reference.oid.format() {
6805                    return Err(GitError::InvalidObjectId(
6806                        "ls-refs peeled object format does not match ref object format".into(),
6807                    ));
6808                }
6809                out.push(' ');
6810                out.push_str("peeled:");
6811                out.push_str(&peeled.to_string());
6812            }
6813            if let Some(target) = &reference.symref_target {
6814                validate_protocol_v2_token("ls-refs symref-target", target)?;
6815                out.push(' ');
6816                out.push_str("symref-target:");
6817                out.push_str(target);
6818            }
6819            append_protocol_v2_ls_refs_attributes(&mut out, &reference.attributes)?;
6820        }
6821        ProtocolV2LsRefsRecord::Unborn {
6822            name,
6823            symref_target,
6824            attributes,
6825        } => {
6826            validate_protocol_v2_token("ls-refs ref name", name)?;
6827            out.push_str("unborn ");
6828            out.push_str(name);
6829            if let Some(target) = symref_target {
6830                validate_protocol_v2_token("ls-refs symref-target", target)?;
6831                out.push(' ');
6832                out.push_str("symref-target:");
6833                out.push_str(target);
6834            }
6835            append_protocol_v2_ls_refs_attributes(&mut out, attributes)?;
6836        }
6837    }
6838    Ok(out)
6839}
6840
6841fn append_protocol_v2_ls_refs_attributes(out: &mut String, attributes: &[String]) -> Result<()> {
6842    for attr in attributes {
6843        validate_protocol_v2_token("ls-refs attribute", attr)?;
6844        if attr.starts_with("peeled:") || attr.starts_with("symref-target:") {
6845            return Err(GitError::InvalidFormat(
6846                "ls-refs generic attributes must not duplicate known attributes".into(),
6847            ));
6848        }
6849        out.push(' ');
6850        out.push_str(attr);
6851    }
6852    Ok(())
6853}
6854
6855fn parse_fetch_section_header(payload: &[u8]) -> Result<String> {
6856    let name = parse_protocol_v2_line_text("fetch response section", payload)?;
6857    validate_capability_name(name)?;
6858    Ok(name.to_string())
6859}
6860
6861fn flush_terminates_protocol_v2_response(frames: &[PktLineFrame], idx: usize) -> bool {
6862    idx + 1 == frames.len()
6863        || (idx + 2 == frames.len() && matches!(frames[idx + 1], PktLineFrame::ResponseEnd))
6864}
6865
6866fn parse_fetch_section(
6867    format: ObjectFormat,
6868    name: String,
6869    lines: Vec<Vec<u8>>,
6870) -> Result<ProtocolV2FetchResponseSection> {
6871    match name.as_str() {
6872        "acknowledgments" => lines
6873            .iter()
6874            .map(|line| parse_fetch_acknowledgment(format, line))
6875            .collect::<Result<Vec<_>>>()
6876            .map(ProtocolV2FetchResponseSection::Acknowledgments),
6877        "shallow-info" => lines
6878            .iter()
6879            .map(|line| parse_fetch_shallow_info(format, line))
6880            .collect::<Result<Vec<_>>>()
6881            .map(ProtocolV2FetchResponseSection::ShallowInfo),
6882        "wanted-refs" => lines
6883            .iter()
6884            .map(|line| parse_fetch_wanted_ref(format, line))
6885            .collect::<Result<Vec<_>>>()
6886            .map(ProtocolV2FetchResponseSection::WantedRefs),
6887        "packfile-uris" => lines
6888            .iter()
6889            .map(|line| parse_fetch_packfile_uri(format, line))
6890            .collect::<Result<Vec<_>>>()
6891            .map(ProtocolV2FetchResponseSection::PackfileUris),
6892        "packfile" => Ok(ProtocolV2FetchResponseSection::Packfile(lines)),
6893        _ => Ok(ProtocolV2FetchResponseSection::Unknown { name, lines }),
6894    }
6895}
6896
6897fn parse_fetch_acknowledgment(
6898    format: ObjectFormat,
6899    line: &[u8],
6900) -> Result<ProtocolV2FetchAcknowledgment> {
6901    let text = parse_protocol_v2_line_text("fetch acknowledgment", line)?;
6902    match text {
6903        "NAK" => Ok(ProtocolV2FetchAcknowledgment::Nak),
6904        "ready" => Ok(ProtocolV2FetchAcknowledgment::Ready),
6905        value if value.starts_with("ACK ") => Ok(ProtocolV2FetchAcknowledgment::Ack(
6906            parse_oid_argument(format, "fetch ACK", value, "ACK ")?,
6907        )),
6908        other => Err(GitError::InvalidFormat(format!(
6909            "unsupported fetch acknowledgment {other}"
6910        ))),
6911    }
6912}
6913
6914fn parse_fetch_shallow_info(
6915    format: ObjectFormat,
6916    line: &[u8],
6917) -> Result<ProtocolV2FetchShallowInfo> {
6918    let text = parse_protocol_v2_line_text("fetch shallow-info", line)?;
6919    if text.starts_with("shallow ") {
6920        return Ok(ProtocolV2FetchShallowInfo::Shallow(parse_oid_argument(
6921            format,
6922            "fetch shallow",
6923            text,
6924            "shallow ",
6925        )?));
6926    }
6927    if text.starts_with("unshallow ") {
6928        return Ok(ProtocolV2FetchShallowInfo::Unshallow(parse_oid_argument(
6929            format,
6930            "fetch unshallow",
6931            text,
6932            "unshallow ",
6933        )?));
6934    }
6935    Err(GitError::InvalidFormat(format!(
6936        "unsupported fetch shallow-info {text}"
6937    )))
6938}
6939
6940fn parse_fetch_wanted_ref(format: ObjectFormat, line: &[u8]) -> Result<ProtocolV2FetchWantedRef> {
6941    let text = parse_protocol_v2_line_text("fetch wanted-ref", line)?;
6942    let (oid, name) = text
6943        .split_once(' ')
6944        .ok_or_else(|| GitError::InvalidFormat("fetch wanted-ref is missing name".into()))?;
6945    validate_protocol_v2_token("fetch wanted-ref name", name)?;
6946    Ok(ProtocolV2FetchWantedRef {
6947        oid: ObjectId::from_hex(format, oid)?,
6948        name: name.to_string(),
6949    })
6950}
6951
6952fn parse_fetch_packfile_uri(
6953    format: ObjectFormat,
6954    line: &[u8],
6955) -> Result<ProtocolV2FetchPackfileUri> {
6956    let text = parse_protocol_v2_line_text("fetch packfile-uri", line)?;
6957    let (pack_hash, uri) = text
6958        .split_once(' ')
6959        .ok_or_else(|| GitError::InvalidFormat("fetch packfile-uri is missing uri".into()))?;
6960    validate_protocol_v2_token("fetch packfile-uri hash", pack_hash)?;
6961    validate_protocol_v2_token("fetch packfile-uri", uri)?;
6962    Ok(ProtocolV2FetchPackfileUri {
6963        pack_hash: ObjectId::from_hex(format, pack_hash)?,
6964        uri: uri.to_string(),
6965    })
6966}
6967
6968fn protocol_v2_fetch_section_name(section: &ProtocolV2FetchResponseSection) -> &str {
6969    match section {
6970        ProtocolV2FetchResponseSection::Acknowledgments(_) => "acknowledgments",
6971        ProtocolV2FetchResponseSection::ShallowInfo(_) => "shallow-info",
6972        ProtocolV2FetchResponseSection::WantedRefs(_) => "wanted-refs",
6973        ProtocolV2FetchResponseSection::PackfileUris(_) => "packfile-uris",
6974        ProtocolV2FetchResponseSection::Packfile(_) => "packfile",
6975        ProtocolV2FetchResponseSection::Unknown { name, .. } => name,
6976    }
6977}
6978
6979fn format_protocol_v2_fetch_section_lines(
6980    section: &ProtocolV2FetchResponseSection,
6981) -> Result<Vec<Vec<u8>>> {
6982    match section {
6983        ProtocolV2FetchResponseSection::Acknowledgments(acks) => acks
6984            .iter()
6985            .map(|ack| match ack {
6986                ProtocolV2FetchAcknowledgment::Nak => Ok(line_from_str("NAK")),
6987                ProtocolV2FetchAcknowledgment::Ack(oid) => Ok(line_from_str(&format!("ACK {oid}"))),
6988                ProtocolV2FetchAcknowledgment::Ready => Ok(line_from_str("ready")),
6989            })
6990            .collect(),
6991        ProtocolV2FetchResponseSection::ShallowInfo(entries) => entries
6992            .iter()
6993            .map(|entry| match entry {
6994                ProtocolV2FetchShallowInfo::Shallow(oid) => {
6995                    Ok(line_from_str(&format!("shallow {oid}")))
6996                }
6997                ProtocolV2FetchShallowInfo::Unshallow(oid) => {
6998                    Ok(line_from_str(&format!("unshallow {oid}")))
6999                }
7000            })
7001            .collect(),
7002        ProtocolV2FetchResponseSection::WantedRefs(refs) => refs
7003            .iter()
7004            .map(|wanted| {
7005                validate_protocol_v2_token("fetch wanted-ref name", &wanted.name)?;
7006                Ok(line_from_str(&format!("{} {}", wanted.oid, wanted.name)))
7007            })
7008            .collect(),
7009        ProtocolV2FetchResponseSection::PackfileUris(uris) => uris
7010            .iter()
7011            .map(|packfile_uri| {
7012                validate_protocol_v2_token("fetch packfile-uri", &packfile_uri.uri)?;
7013                Ok(line_from_str(&format!(
7014                    "{} {}",
7015                    packfile_uri.pack_hash, packfile_uri.uri
7016                )))
7017            })
7018            .collect(),
7019        ProtocolV2FetchResponseSection::Packfile(lines) => Ok(lines.clone()),
7020        ProtocolV2FetchResponseSection::Unknown { name, lines } => {
7021            validate_capability_name(name)?;
7022            for line in lines {
7023                validate_protocol_v2_line("fetch unknown section line", line)?;
7024            }
7025            Ok(lines.clone())
7026        }
7027    }
7028}
7029
7030fn parse_protocol_v2_object_info_record(
7031    format: ObjectFormat,
7032    line: &[u8],
7033) -> Result<ProtocolV2ObjectInfoRecord> {
7034    let text = parse_protocol_v2_line_text("object-info record", line)?;
7035    let mut fields = text.split(' ');
7036    let oid = fields
7037        .next()
7038        .ok_or_else(|| GitError::InvalidFormat("object-info record is missing oid".into()))?;
7039    let size = fields
7040        .next()
7041        .ok_or_else(|| GitError::InvalidFormat("object-info record is missing size".into()))?;
7042    if fields.next().is_some() {
7043        return Err(GitError::InvalidFormat(
7044            "object-info record has too many fields".into(),
7045        ));
7046    }
7047    validate_protocol_v2_token("object-info oid", oid)?;
7048    validate_protocol_v2_token("object-info size", size)?;
7049    let size = size
7050        .parse::<u64>()
7051        .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
7052    Ok(ProtocolV2ObjectInfoRecord {
7053        oid: ObjectId::from_hex(format, oid)?,
7054        size,
7055    })
7056}
7057
7058fn parse_dumb_http_info_ref_record(format: ObjectFormat, line: &[u8]) -> Result<DumbHttpRefRecord> {
7059    validate_dumb_http_info_ref_line(line)?;
7060    let line = trim_trailing_lf(line);
7061    let tab = line
7062        .iter()
7063        .position(|byte| *byte == b'\t')
7064        .ok_or_else(|| GitError::InvalidFormat("dumb HTTP ref record is missing name".into()))?;
7065    let (oid, name) = (&line[..tab], &line[tab + 1..]);
7066    let oid = std::str::from_utf8(oid).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
7067    validate_protocol_v2_token("dumb HTTP ref oid", oid)?;
7068    let name = std::str::from_utf8(name).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
7069    let (name, peeled) = name
7070        .strip_suffix("^{}")
7071        .map_or((name, false), |name| (name, true));
7072    validate_dumb_http_ref_name(name)?;
7073    Ok(DumbHttpRefRecord {
7074        oid: ObjectId::from_hex(format, oid)?,
7075        name: name.to_string(),
7076        peeled,
7077    })
7078}
7079
7080fn parse_dumb_http_alternate(line: &[u8]) -> Result<String> {
7081    validate_dumb_http_alternate_line(line)?;
7082    let line = trim_trailing_lf(line);
7083    let alternate =
7084        std::str::from_utf8(line).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
7085    validate_dumb_http_alternate(alternate)?;
7086    Ok(alternate.to_string())
7087}
7088
7089fn parse_dumb_http_pack_record(format: ObjectFormat, line: &[u8]) -> Result<DumbHttpPackRecord> {
7090    validate_dumb_http_info_ref_line(line)?;
7091    let line = parse_protocol_v2_line_text("dumb HTTP pack record", line)?;
7092    let pack_name = line
7093        .strip_prefix("P ")
7094        .ok_or_else(|| GitError::InvalidFormat("dumb HTTP pack record must start with P".into()))?;
7095    let hash = pack_name
7096        .strip_prefix("pack-")
7097        .and_then(|value| value.strip_suffix(".pack"))
7098        .ok_or_else(|| GitError::InvalidFormat("invalid dumb HTTP pack name".into()))?;
7099    validate_protocol_v2_token("dumb HTTP pack hash", hash)?;
7100    Ok(DumbHttpPackRecord {
7101        hash: ObjectId::from_hex(format, hash)?,
7102    })
7103}
7104
7105fn encode_protocol_v2_capability(capability: &Capability) -> Result<Vec<u8>> {
7106    validate_capability_name(&capability.name)?;
7107    let mut out = capability.name.as_bytes().to_vec();
7108    if let Some(value) = &capability.value {
7109        validate_protocol_v2_capability_value(value)?;
7110        out.push(b'=');
7111        out.extend_from_slice(value.as_bytes());
7112    }
7113    Ok(out)
7114}
7115
7116fn validate_capability_field(label: &str, value: &str) -> Result<()> {
7117    if value.is_empty() {
7118        return Err(GitError::InvalidFormat(format!("{label} is empty")));
7119    }
7120    if value
7121        .bytes()
7122        .any(|byte| matches!(byte, b' ' | b'\n' | b'\r' | b'\t' | 0))
7123    {
7124        return Err(GitError::InvalidFormat(format!(
7125            "{label} contains a delimiter byte"
7126        )));
7127    }
7128    Ok(())
7129}
7130
7131fn validate_capability_name(value: &str) -> Result<()> {
7132    validate_capability_field("capability name", value)?;
7133    if value.bytes().any(|byte| byte == b'=') {
7134        return Err(GitError::InvalidFormat(
7135            "capability name contains a delimiter byte".into(),
7136        ));
7137    }
7138    Ok(())
7139}
7140
7141fn validate_protocol_v2_capability_value(value: &str) -> Result<()> {
7142    if value.is_empty() {
7143        return Err(GitError::InvalidFormat(
7144            "protocol v2 capability value is empty".into(),
7145        ));
7146    }
7147    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
7148        return Err(GitError::InvalidFormat(
7149            "protocol v2 capability value contains a delimiter byte".into(),
7150        ));
7151    }
7152    Ok(())
7153}
7154
7155fn validate_protocol_v2_argument(value: &[u8]) -> Result<()> {
7156    if value.is_empty() {
7157        return Err(GitError::InvalidFormat(
7158            "protocol v2 command argument is empty".into(),
7159        ));
7160    }
7161    if value.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
7162        return Err(GitError::InvalidFormat(
7163            "protocol v2 command argument contains a delimiter byte".into(),
7164        ));
7165    }
7166    Ok(())
7167}
7168
7169fn validate_upload_archive_argument(value: &str) -> Result<()> {
7170    if value.is_empty() {
7171        return Err(GitError::InvalidFormat(
7172            "upload-archive argument is empty".into(),
7173        ));
7174    }
7175    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
7176        return Err(GitError::InvalidFormat(
7177            "upload-archive argument contains a delimiter byte".into(),
7178        ));
7179    }
7180    Ok(())
7181}
7182
7183fn validate_upload_archive_status_message(value: &str) -> Result<()> {
7184    if value.is_empty() {
7185        return Err(GitError::InvalidFormat(
7186            "upload-archive status message is empty".into(),
7187        ));
7188    }
7189    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
7190        return Err(GitError::InvalidFormat(
7191            "upload-archive status message contains a delimiter byte".into(),
7192        ));
7193    }
7194    Ok(())
7195}
7196
7197fn non_empty(value: &str) -> Option<&str> {
7198    (!value.is_empty()).then_some(value)
7199}
7200
7201fn validate_refspec_value(value: &str) -> Result<()> {
7202    if value.is_empty() {
7203        return Err(GitError::InvalidFormat("refspec is empty".into()));
7204    }
7205    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
7206        return Err(GitError::InvalidFormat(
7207            "refspec contains a delimiter byte".into(),
7208        ));
7209    }
7210    Ok(())
7211}
7212
7213fn validate_refspec_endpoint(label: &str, value: &str) -> Result<()> {
7214    if value.is_empty() {
7215        return Err(GitError::InvalidFormat(format!("{label} is empty")));
7216    }
7217    if value
7218        .bytes()
7219        .any(|byte| matches!(byte, b':' | b' ' | b'\t' | b'\n' | b'\r' | 0))
7220    {
7221        return Err(GitError::InvalidFormat(format!(
7222            "{label} contains a delimiter byte"
7223        )));
7224    }
7225    Ok(())
7226}
7227
7228fn count_refspec_wildcards(value: &str) -> usize {
7229    value.bytes().filter(|byte| *byte == b'*').count()
7230}
7231
7232fn validate_refspec_shape(refspec: &RefSpec) -> Result<()> {
7233    if refspec.force && refspec.negative {
7234        return Err(GitError::InvalidFormat(
7235            "negative refspec must not be forced".into(),
7236        ));
7237    }
7238    if refspec.negative && refspec.dst.is_some() {
7239        return Err(GitError::InvalidFormat(
7240            "negative refspec must not have a destination".into(),
7241        ));
7242    }
7243    if refspec.negative && refspec.src.is_none() {
7244        return Err(GitError::InvalidFormat(
7245            "negative refspec is missing a source".into(),
7246        ));
7247    }
7248    if refspec.src.is_none() && refspec.dst.is_none() && refspec.negative {
7249        return Err(GitError::InvalidFormat(
7250            "refspec must include a source or destination".into(),
7251        ));
7252    }
7253    if let Some(src) = &refspec.src {
7254        validate_refspec_endpoint("refspec source", src)?;
7255    }
7256    if let Some(dst) = &refspec.dst {
7257        validate_refspec_endpoint("refspec destination", dst)?;
7258    }
7259    let src_pattern_count = refspec
7260        .src
7261        .as_deref()
7262        .map(count_refspec_wildcards)
7263        .unwrap_or(0);
7264    let dst_pattern_count = refspec
7265        .dst
7266        .as_deref()
7267        .map(count_refspec_wildcards)
7268        .unwrap_or(0);
7269    if src_pattern_count > 1 || dst_pattern_count > 1 {
7270        return Err(GitError::InvalidFormat(
7271            "refspec endpoint has too many wildcards".into(),
7272        ));
7273    }
7274    if refspec.dst.is_some() && (src_pattern_count == 1) != (dst_pattern_count == 1) {
7275        return Err(GitError::InvalidFormat(
7276            "refspec wildcard must appear in both source and destination".into(),
7277        ));
7278    }
7279    if refspec.pattern != (src_pattern_count == 1 || dst_pattern_count == 1) {
7280        return Err(GitError::InvalidFormat(
7281            "refspec pattern flag does not match endpoints".into(),
7282        ));
7283    }
7284    Ok(())
7285}
7286
7287fn parse_fetch_head_record(format: ObjectFormat, line: &[u8]) -> Result<FetchHeadRecord> {
7288    validate_fetch_head_line(line)?;
7289    let line = trim_trailing_lf(line);
7290    let mut fields = line.splitn(3, |byte| *byte == b'\t');
7291    let oid = fields
7292        .next()
7293        .ok_or_else(|| GitError::InvalidFormat("FETCH_HEAD record is missing oid".into()))?;
7294    let merge_marker = fields.next().ok_or_else(|| {
7295        GitError::InvalidFormat("FETCH_HEAD record is missing merge marker".into())
7296    })?;
7297    let description = fields.next().ok_or_else(|| {
7298        GitError::InvalidFormat("FETCH_HEAD record is missing description".into())
7299    })?;
7300    let oid = std::str::from_utf8(oid).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
7301    validate_protocol_v2_token("FETCH_HEAD oid", oid)?;
7302    let not_for_merge = match merge_marker {
7303        b"" => false,
7304        b"not-for-merge" => true,
7305        _ => {
7306            return Err(GitError::InvalidFormat(
7307                "FETCH_HEAD record has invalid merge marker".into(),
7308            ));
7309        }
7310    };
7311    let description =
7312        std::str::from_utf8(description).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
7313    validate_fetch_head_description_field(description)?;
7314    Ok(FetchHeadRecord {
7315        oid: ObjectId::from_hex(format, oid)?,
7316        not_for_merge,
7317        description: description.to_string(),
7318    })
7319}
7320
7321fn validate_fetch_head_line(value: &[u8]) -> Result<()> {
7322    if value.is_empty() {
7323        return Err(GitError::InvalidFormat("FETCH_HEAD record is empty".into()));
7324    }
7325    if !value.ends_with(b"\n") {
7326        return Err(GitError::InvalidFormat(
7327            "FETCH_HEAD record missing LF".into(),
7328        ));
7329    }
7330    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7331        return Err(GitError::InvalidFormat(
7332            "FETCH_HEAD record contains a delimiter byte".into(),
7333        ));
7334    }
7335    Ok(())
7336}
7337
7338fn validate_fetch_head_description_field(value: &str) -> Result<()> {
7339    if value.is_empty() {
7340        return Err(GitError::InvalidFormat(
7341            "FETCH_HEAD description is empty".into(),
7342        ));
7343    }
7344    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
7345        return Err(GitError::InvalidFormat(
7346            "FETCH_HEAD description contains a delimiter byte".into(),
7347        ));
7348    }
7349    Ok(())
7350}
7351
7352fn refspec_is_excluded(negative: &[&RefSpec], source: &str) -> Result<bool> {
7353    for refspec in negative {
7354        if refspec_matches_source(refspec, source)? {
7355            return Ok(true);
7356        }
7357    }
7358    Ok(false)
7359}
7360
7361fn zero_object_id(format: ObjectFormat) -> Result<ObjectId> {
7362    Ok(ObjectId::null(format))
7363}
7364
7365fn local_ref<'a>(refs: &'a [PushSourceRef], name: &str) -> Option<&'a PushSourceRef> {
7366    refs.iter().find(|reference| reference.name == name)
7367}
7368
7369fn remote_ref<'a>(refs: &'a [RefAdvertisement], name: &str) -> Option<&'a RefAdvertisement> {
7370    refs.iter().find(|reference| reference.name == name)
7371}
7372
7373fn validate_push_source_ref(format: ObjectFormat, reference: &PushSourceRef) -> Result<()> {
7374    if reference.oid.format() != format {
7375        return Err(GitError::InvalidObjectId(
7376            "push source ref object format does not match repository".into(),
7377        ));
7378    }
7379    validate_refspec_endpoint("push source ref name", &reference.name)
7380}
7381
7382fn require_receive_pack_feature(advertised: bool, name: &str) -> Result<()> {
7383    if advertised {
7384        Ok(())
7385    } else {
7386        Err(GitError::InvalidFormat(format!(
7387            "receive-pack feature {name} was not advertised"
7388        )))
7389    }
7390}
7391
7392fn validate_smart_http_service(service: GitService) -> Result<()> {
7393    match service {
7394        GitService::UploadPack | GitService::ReceivePack => Ok(()),
7395        GitService::UploadArchive => Err(GitError::InvalidFormat(
7396            "smart HTTP only supports upload-pack and receive-pack services".into(),
7397        )),
7398    }
7399}
7400
7401fn normalize_http_repository_path(path: &str) -> Result<String> {
7402    if path.is_empty() {
7403        return Err(GitError::InvalidFormat(
7404            "smart HTTP repository path is empty".into(),
7405        ));
7406    }
7407    if !path.starts_with('/') {
7408        return Err(GitError::InvalidFormat(
7409            "smart HTTP repository path must start with /".into(),
7410        ));
7411    }
7412    if path
7413        .bytes()
7414        .any(|byte| matches!(byte, b'\n' | b'\r' | 0 | b'?' | b'#'))
7415    {
7416        return Err(GitError::InvalidFormat(
7417            "smart HTTP repository path contains a delimiter byte".into(),
7418        ));
7419    }
7420    let normalized = path.trim_end_matches('/');
7421    Ok(if normalized.is_empty() {
7422        "/".into()
7423    } else {
7424        normalized.to_string()
7425    })
7426}
7427
7428fn dumb_http_pack_resource_path(
7429    repository_path: &str,
7430    hash: &ObjectId,
7431    suffix: &str,
7432) -> Result<String> {
7433    let repository_path = normalize_http_repository_path(repository_path)?;
7434    Ok(format!(
7435        "{repository_path}/objects/pack/pack-{hash}.{suffix}"
7436    ))
7437}
7438
7439fn parse_smart_http_content_type(value: &str, suffix: &str) -> Result<GitService> {
7440    let value = value.trim();
7441    if value.is_empty() {
7442        return Err(GitError::InvalidFormat(
7443            "smart HTTP content type is empty".into(),
7444        ));
7445    }
7446    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
7447        return Err(GitError::InvalidFormat(
7448            "smart HTTP content type contains a delimiter byte".into(),
7449        ));
7450    }
7451    let value = value.to_ascii_lowercase();
7452    let service = value
7453        .strip_prefix("application/x-")
7454        .and_then(|value| value.strip_suffix(suffix))
7455        .ok_or_else(|| GitError::InvalidFormat("invalid smart HTTP content type".into()))?;
7456    let service = parse_git_service(service)?;
7457    validate_smart_http_service(service)?;
7458    Ok(service)
7459}
7460
7461fn validate_dumb_http_info_ref_line(value: &[u8]) -> Result<()> {
7462    if value.is_empty() {
7463        return Err(GitError::InvalidFormat(
7464            "dumb HTTP ref record is empty".into(),
7465        ));
7466    }
7467    if !value.ends_with(b"\n") {
7468        return Err(GitError::InvalidFormat(
7469            "dumb HTTP ref record missing LF".into(),
7470        ));
7471    }
7472    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7473        return Err(GitError::InvalidFormat(
7474            "dumb HTTP ref record contains a delimiter byte".into(),
7475        ));
7476    }
7477    Ok(())
7478}
7479
7480fn validate_dumb_http_ref_name(value: &str) -> Result<()> {
7481    validate_protocol_v2_token("dumb HTTP ref name", value)?;
7482    if value.ends_with("^{}") {
7483        return Err(GitError::InvalidFormat(
7484            "dumb HTTP ref name must not include peeled suffix".into(),
7485        ));
7486    }
7487    Ok(())
7488}
7489
7490fn validate_dumb_http_alternate_line(value: &[u8]) -> Result<()> {
7491    if value.is_empty() {
7492        return Err(GitError::InvalidFormat(
7493            "dumb HTTP alternate is empty".into(),
7494        ));
7495    }
7496    if !value.ends_with(b"\n") {
7497        return Err(GitError::InvalidFormat(
7498            "dumb HTTP alternate missing LF".into(),
7499        ));
7500    }
7501    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7502        return Err(GitError::InvalidFormat(
7503            "dumb HTTP alternate contains a delimiter byte".into(),
7504        ));
7505    }
7506    Ok(())
7507}
7508
7509fn validate_dumb_http_alternate(value: &str) -> Result<()> {
7510    if value.is_empty() {
7511        return Err(GitError::InvalidFormat(
7512            "dumb HTTP alternate is empty".into(),
7513        ));
7514    }
7515    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
7516        return Err(GitError::InvalidFormat(
7517            "dumb HTTP alternate contains a delimiter byte".into(),
7518        ));
7519    }
7520    Ok(())
7521}
7522
7523fn validate_protocol_v2_token(label: &str, value: &str) -> Result<()> {
7524    if value.is_empty() {
7525        return Err(GitError::InvalidFormat(format!("{label} is empty")));
7526    }
7527    if value
7528        .bytes()
7529        .any(|byte| matches!(byte, b' ' | b'\n' | b'\r' | 0))
7530    {
7531        return Err(GitError::InvalidFormat(format!(
7532            "{label} contains a delimiter byte"
7533        )));
7534    }
7535    Ok(())
7536}
7537
7538fn validate_protocol_v2_line(label: &str, value: &[u8]) -> Result<()> {
7539    if value.is_empty() {
7540        return Err(GitError::InvalidFormat(format!("{label} is empty")));
7541    }
7542    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7543        return Err(GitError::InvalidFormat(format!(
7544            "{label} contains a delimiter byte"
7545        )));
7546    }
7547    Ok(())
7548}
7549
7550fn parse_protocol_v2_line_text<'a>(label: &str, value: &'a [u8]) -> Result<&'a str> {
7551    validate_protocol_v2_line(label, value)?;
7552    let value = trim_trailing_lf(value);
7553    if value.is_empty() {
7554        return Err(GitError::InvalidFormat(format!("{label} is empty")));
7555    }
7556    if value.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
7557        return Err(GitError::InvalidFormat(format!(
7558            "{label} contains a delimiter byte"
7559        )));
7560    }
7561    std::str::from_utf8(value).map_err(|err| GitError::InvalidFormat(err.to_string()))
7562}
7563
7564fn parse_oid_argument(
7565    format: ObjectFormat,
7566    label: &str,
7567    value: &str,
7568    prefix: &str,
7569) -> Result<ObjectId> {
7570    let oid = value
7571        .strip_prefix(prefix)
7572        .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
7573    validate_protocol_v2_token(label, oid)?;
7574    ObjectId::from_hex(format, oid)
7575}
7576
7577fn parse_u32_argument(label: &str, value: &str, prefix: &str) -> Result<u32> {
7578    let number = value
7579        .strip_prefix(prefix)
7580        .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
7581    validate_protocol_v2_token(label, number)?;
7582    let parsed = number
7583        .parse::<u32>()
7584        .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
7585    if parsed == 0 {
7586        return Err(GitError::InvalidFormat(format!("{label} must be positive")));
7587    }
7588    Ok(parsed)
7589}
7590
7591fn parse_u64_argument(label: &str, value: &str, prefix: &str) -> Result<u64> {
7592    let number = value
7593        .strip_prefix(prefix)
7594        .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
7595    validate_protocol_v2_token(label, number)?;
7596    number
7597        .parse::<u64>()
7598        .map_err(|err| GitError::InvalidFormat(err.to_string()))
7599}
7600
7601fn line(mut payload: Vec<u8>) -> Vec<u8> {
7602    payload.push(b'\n');
7603    payload
7604}
7605
7606fn line_from_str(payload: &str) -> Vec<u8> {
7607    line(payload.as_bytes().to_vec())
7608}
7609
7610fn trim_trailing_lf(input: &[u8]) -> &[u8] {
7611    input.strip_suffix(b"\n").unwrap_or(input)
7612}
7613
7614#[cfg(test)]
7615mod tests {
7616    use super::*;
7617
7618    #[test]
7619    fn pkt_line_frame_encodes_data_and_control_frames() {
7620        assert_eq!(PktLine(b"hello\n".to_vec()).encode(), b"000ahello\n");
7621        assert_eq!(
7622            PktLineFrame::data(b"hello\n".to_vec())
7623                .expect("test operation should succeed")
7624                .encode(),
7625            b"000ahello\n"
7626        );
7627        assert_eq!(PktLineFrame::Flush.encode(), b"0000");
7628        assert_eq!(PktLineFrame::Delimiter.encode(), b"0001");
7629        assert_eq!(PktLineFrame::ResponseEnd.encode(), b"0002");
7630        assert_eq!(
7631            PktLineFrame::data(b"hello\n".to_vec())
7632                .expect("test operation should succeed")
7633                .try_encode()
7634                .expect("test operation should succeed"),
7635            b"000ahello\n"
7636        );
7637    }
7638
7639    #[test]
7640    fn pkt_line_frame_parses_data_and_control_frames() {
7641        assert_eq!(
7642            PktLineFrame::parse(b"000ahello\n").expect("test operation should succeed"),
7643            (PktLineFrame::Data(b"hello\n".to_vec()), 10)
7644        );
7645        assert_eq!(
7646            PktLineFrame::parse(b"0000").expect("test operation should succeed"),
7647            (PktLineFrame::Flush, 4)
7648        );
7649        assert_eq!(
7650            PktLineFrame::parse(b"0001").expect("test operation should succeed"),
7651            (PktLineFrame::Delimiter, 4)
7652        );
7653        assert_eq!(
7654            PktLineFrame::parse(b"0002").expect("test operation should succeed"),
7655            (PktLineFrame::ResponseEnd, 4)
7656        );
7657    }
7658
7659    #[test]
7660    fn pkt_line_stream_parses_multiple_frames() {
7661        let frames = parse_pkt_line_stream(b"000eversion 2\n00010009done\n0000")
7662            .expect("test operation should succeed");
7663        assert_eq!(
7664            frames,
7665            vec![
7666                PktLineFrame::Data(b"version 2\n".to_vec()),
7667                PktLineFrame::Delimiter,
7668                PktLineFrame::Data(b"done\n".to_vec()),
7669                PktLineFrame::Flush,
7670            ]
7671        );
7672    }
7673
7674    #[test]
7675    fn pkt_line_stream_reads_and_writes_incremental_io() {
7676        let frames = vec![
7677            PktLineFrame::Data(b"version 2\n".to_vec()),
7678            PktLineFrame::Delimiter,
7679            PktLineFrame::Data(b"done\n".to_vec()),
7680            PktLineFrame::Flush,
7681        ];
7682        let mut encoded = Vec::new();
7683        write_pkt_line_frames(&mut encoded, &frames).expect("test operation should succeed");
7684        assert_eq!(encoded, b"000eversion 2\n00010009done\n0000");
7685        assert_eq!(
7686            read_pkt_line_frames(&mut encoded.as_slice()).expect("test operation should succeed"),
7687            frames
7688        );
7689
7690        let mut empty: &[u8] = b"";
7691        assert_eq!(
7692            read_pkt_line_frame(&mut empty).expect("test operation should succeed"),
7693            None
7694        );
7695    }
7696
7697    #[test]
7698    fn pkt_line_stream_reads_until_control_packets() {
7699        let input = b"000eversion 2\n0000trailing";
7700        let frames = read_pkt_line_frames_until_flush(&mut &input[..])
7701            .expect("test operation should succeed");
7702        assert_eq!(
7703            frames,
7704            vec![
7705                PktLineFrame::Data(b"version 2\n".to_vec()),
7706                PktLineFrame::Flush,
7707            ]
7708        );
7709
7710        let input = b"0009done\n0002next";
7711        let frames = read_pkt_line_frames_until_response_end(&mut &input[..])
7712            .expect("test operation should succeed");
7713        assert_eq!(
7714            frames,
7715            vec![
7716                PktLineFrame::Data(b"done\n".to_vec()),
7717                PktLineFrame::ResponseEnd,
7718            ]
7719        );
7720        assert!(read_pkt_line_frames_until_flush(&mut &b"0009done\n"[..]).is_err());
7721    }
7722
7723    #[test]
7724    fn pkt_line_rejects_invalid_lengths() {
7725        assert!(PktLineFrame::parse(b"000").is_err());
7726        assert!(PktLineFrame::parse(b"0003").is_err());
7727        assert!(PktLineFrame::parse(b"000ahello").is_err());
7728        assert!(PktLineFrame::parse(b"zzzz").is_err());
7729        assert!(read_pkt_line_frame(&mut &b"000"[..]).is_err());
7730        assert!(read_pkt_line_frame(&mut &b"0003"[..]).is_err());
7731    }
7732
7733    #[test]
7734    fn pkt_line_rejects_oversized_data() {
7735        let payload = vec![b'x'; PKT_LINE_MAX_PAYLOAD_LEN + 1];
7736        assert!(PktLineFrame::data(payload.clone()).is_err());
7737        assert!(PktLine(payload.clone()).try_encode().is_err());
7738        assert!(PktLineFrame::Data(payload.clone()).try_encode().is_err());
7739        assert!(write_pkt_line_frame(&mut Vec::new(), &PktLineFrame::Data(payload)).is_err());
7740        assert!(PktLineFrame::parse(b"fff1").is_err());
7741    }
7742
7743    #[test]
7744    fn protocol_error_lines_parse_encode_and_stream() {
7745        let error = parse_error_line(b"ERR remote rejected request\n")
7746            .expect("test operation should succeed");
7747        assert_eq!(
7748            error,
7749            ProtocolErrorLine {
7750                message: "remote rejected request".into(),
7751            }
7752        );
7753        assert_eq!(
7754            encode_error_line(&error).expect("test operation should succeed"),
7755            b"ERR remote rejected request\n"
7756        );
7757        assert_eq!(
7758            parse_error_frame(&PktLineFrame::Data(
7759                b"ERR remote rejected request\n".to_vec()
7760            ))
7761            .expect("test operation should succeed"),
7762            Some(error.clone())
7763        );
7764        assert_eq!(
7765            parse_error_frame(&PktLineFrame::Data(b"NAK\n".to_vec()))
7766                .expect("test operation should succeed"),
7767            None
7768        );
7769
7770        let mut encoded = Vec::new();
7771        write_error_line(&mut encoded, &error).expect("test operation should succeed");
7772        encoded.extend_from_slice(b"tail");
7773        let mut input = encoded.as_slice();
7774        assert_eq!(
7775            read_error_line(&mut input).expect("test operation should succeed"),
7776            error
7777        );
7778        assert_eq!(input, b"tail");
7779    }
7780
7781    #[test]
7782    fn protocol_error_lines_reject_malformed_messages() {
7783        assert!(parse_error_line(b"ERR\n").is_err());
7784        assert!(parse_error_line(b"ERR \n").is_err());
7785        assert!(parse_error_line(b"ERR bad\0message\n").is_err());
7786        assert!(parse_error_line(b"NAK\n").is_err());
7787        assert!(
7788            encode_error_line(&ProtocolErrorLine {
7789                message: "bad\nmessage".into(),
7790            })
7791            .is_err()
7792        );
7793        assert!(read_error_line(&mut &b"0000"[..]).is_err());
7794    }
7795
7796    #[test]
7797    fn refspec_parser_handles_fetch_push_and_negative_forms() {
7798        assert_eq!(
7799            parse_refspec("+refs/heads/*:refs/remotes/origin/*")
7800                .expect("test operation should succeed"),
7801            RefSpec {
7802                force: true,
7803                negative: false,
7804                src: Some("refs/heads/*".into()),
7805                dst: Some("refs/remotes/origin/*".into()),
7806                pattern: true,
7807            }
7808        );
7809        assert_eq!(
7810            parse_refspec("refs/heads/main").expect("test operation should succeed"),
7811            RefSpec {
7812                force: false,
7813                negative: false,
7814                src: Some("refs/heads/main".into()),
7815                dst: None,
7816                pattern: false,
7817            }
7818        );
7819        assert_eq!(
7820            parse_refspec(":refs/heads/topic").expect("test operation should succeed"),
7821            RefSpec {
7822                force: false,
7823                negative: false,
7824                src: None,
7825                dst: Some("refs/heads/topic".into()),
7826                pattern: false,
7827            }
7828        );
7829        assert_eq!(
7830            parse_refspec(":").expect("test operation should succeed"),
7831            RefSpec {
7832                force: false,
7833                negative: false,
7834                src: None,
7835                dst: None,
7836                pattern: false,
7837            }
7838        );
7839        assert_eq!(
7840            parse_refspec("^refs/tags/private/*").expect("test operation should succeed"),
7841            RefSpec {
7842                force: false,
7843                negative: true,
7844                src: Some("refs/tags/private/*".into()),
7845                dst: None,
7846                pattern: true,
7847            }
7848        );
7849    }
7850
7851    #[test]
7852    fn refspec_encode_and_map_sources() {
7853        let pattern = parse_refspec("+refs/heads/*:refs/remotes/origin/*")
7854            .expect("test operation should succeed");
7855        assert_eq!(
7856            encode_refspec(&pattern).expect("test operation should succeed"),
7857            "+refs/heads/*:refs/remotes/origin/*"
7858        );
7859        assert!(
7860            refspec_matches_source(&pattern, "refs/heads/main")
7861                .expect("test operation should succeed")
7862        );
7863        assert_eq!(
7864            refspec_map_source(&pattern, "refs/heads/main").expect("test operation should succeed"),
7865            Some("refs/remotes/origin/main".into())
7866        );
7867        assert_eq!(
7868            refspec_map_source(&pattern, "refs/tags/v1").expect("test operation should succeed"),
7869            None
7870        );
7871
7872        let direct = parse_refspec("HEAD:refs/heads/main").expect("test operation should succeed");
7873        assert_eq!(
7874            encode_refspec(&direct).expect("test operation should succeed"),
7875            "HEAD:refs/heads/main"
7876        );
7877        assert_eq!(
7878            refspec_map_source(&direct, "HEAD").expect("test operation should succeed"),
7879            Some("refs/heads/main".into())
7880        );
7881
7882        let delete = parse_refspec(":refs/heads/old").expect("test operation should succeed");
7883        assert_eq!(
7884            encode_refspec(&delete).expect("test operation should succeed"),
7885            ":refs/heads/old"
7886        );
7887        assert_eq!(
7888            refspec_map_source(&delete, "HEAD").expect("test operation should succeed"),
7889            None
7890        );
7891
7892        let matching = parse_refspec(":").expect("test operation should succeed");
7893        assert_eq!(
7894            encode_refspec(&matching).expect("test operation should succeed"),
7895            ":"
7896        );
7897    }
7898
7899    #[test]
7900    fn refspec_parser_rejects_malformed_values() {
7901        assert!(parse_refspec("").is_err());
7902        assert!(parse_refspec("+^refs/heads/main").is_err());
7903        assert!(parse_refspec("^refs/heads/main:refs/remotes/origin/main").is_err());
7904        assert!(parse_refspec("refs/heads/*:refs/remotes/origin/main").is_err());
7905        assert!(parse_refspec("refs/heads/**:refs/remotes/origin/*").is_err());
7906        assert!(parse_refspec("refs/heads/main:refs/remotes/origin/main:extra").is_err());
7907        assert!(parse_refspec("refs/heads/main\n").is_err());
7908        assert!(
7909            encode_refspec(&RefSpec {
7910                force: false,
7911                negative: false,
7912                src: Some("refs/heads/*".into()),
7913                dst: Some("refs/remotes/origin/main".into()),
7914                pattern: true,
7915            })
7916            .is_err()
7917        );
7918    }
7919
7920    #[test]
7921    fn fetch_head_records_parse_encode_and_describe_refs() {
7922        let first = ObjectId::from_hex(
7923            ObjectFormat::Sha1,
7924            "1111111111111111111111111111111111111111",
7925        )
7926        .expect("test operation should succeed");
7927        let second = ObjectId::from_hex(
7928            ObjectFormat::Sha1,
7929            "2222222222222222222222222222222222222222",
7930        )
7931        .expect("test operation should succeed");
7932        let input = b"1111111111111111111111111111111111111111\t\tbranch 'main' of ../bundle.bdl\n2222222222222222222222222222222222222222\tnot-for-merge\ttag 'v1' of ../bundle.bdl\n";
7933        let records =
7934            parse_fetch_head(ObjectFormat::Sha1, input).expect("test operation should succeed");
7935        assert_eq!(
7936            records,
7937            vec![
7938                FetchHeadRecord {
7939                    oid: first,
7940                    not_for_merge: false,
7941                    description: "branch 'main' of ../bundle.bdl".into(),
7942                },
7943                FetchHeadRecord {
7944                    oid: second,
7945                    not_for_merge: true,
7946                    description: "tag 'v1' of ../bundle.bdl".into(),
7947                },
7948            ]
7949        );
7950        assert_eq!(
7951            encode_fetch_head(&records).expect("test operation should succeed"),
7952            input
7953        );
7954        assert_eq!(
7955            parse_fetch_head(ObjectFormat::Sha1, b"").expect("test operation should succeed"),
7956            Vec::<FetchHeadRecord>::new()
7957        );
7958        assert_eq!(
7959            fetch_head_remote_description("refs/heads/main", "../bundle.bdl")
7960                .expect("test operation should succeed"),
7961            "branch 'main' of ../bundle.bdl"
7962        );
7963        assert_eq!(
7964            fetch_head_remote_description("refs/tags/v1", "../bundle.bdl")
7965                .expect("test operation should succeed"),
7966            "tag 'v1' of ../bundle.bdl"
7967        );
7968        // A bare `HEAD` fetch records just the URL — git emits an empty note.
7969        assert_eq!(
7970            fetch_head_remote_description("HEAD", "../bundle.bdl")
7971                .expect("test operation should succeed"),
7972            "../bundle.bdl"
7973        );
7974    }
7975
7976    #[test]
7977    fn fetch_head_records_streams_round_trip() {
7978        let records = vec![FetchHeadRecord {
7979            oid: ObjectId::from_hex(
7980                ObjectFormat::Sha1,
7981                "1111111111111111111111111111111111111111",
7982            )
7983            .expect("test operation should succeed"),
7984            not_for_merge: false,
7985            description: "branch 'main' of ../bundle.bdl".into(),
7986        }];
7987        let mut encoded = Vec::new();
7988        write_fetch_head(&mut encoded, &records).expect("test operation should succeed");
7989        let mut input = encoded.as_slice();
7990        assert_eq!(
7991            read_fetch_head(ObjectFormat::Sha1, &mut input).expect("test operation should succeed"),
7992            records
7993        );
7994        assert!(input.is_empty());
7995    }
7996
7997    #[test]
7998    fn fetch_head_records_reject_malformed_lines() {
7999        assert!(
8000            parse_fetch_head(
8001                ObjectFormat::Sha1,
8002                b"1111111111111111111111111111111111111111\t\tbranch 'main'"
8003            )
8004            .is_err()
8005        );
8006        assert!(
8007            parse_fetch_head(
8008                ObjectFormat::Sha1,
8009                b"1111111111111111111111111111111111111111\tfor-merge\tbranch 'main'\n"
8010            )
8011            .is_err()
8012        );
8013        assert!(parse_fetch_head(ObjectFormat::Sha1, b"not-a-hash\t\tbranch 'main'\n").is_err());
8014        assert!(
8015            encode_fetch_head(&[FetchHeadRecord {
8016                oid: ObjectId::from_hex(
8017                    ObjectFormat::Sha1,
8018                    "1111111111111111111111111111111111111111"
8019                )
8020                .expect("test operation should succeed"),
8021                not_for_merge: false,
8022                description: "bad\ndescription".into(),
8023            }])
8024            .is_err()
8025        );
8026    }
8027
8028    #[test]
8029    fn fetch_planner_maps_direct_pattern_and_negative_refspecs() {
8030        let main = ObjectId::from_hex(
8031            ObjectFormat::Sha1,
8032            "1111111111111111111111111111111111111111",
8033        )
8034        .expect("test operation should succeed");
8035        let next = ObjectId::from_hex(
8036            ObjectFormat::Sha1,
8037            "2222222222222222222222222222222222222222",
8038        )
8039        .expect("test operation should succeed");
8040        let refs = vec![
8041            RefAdvertisement {
8042                oid: main.clone(),
8043                name: "refs/heads/main".into(),
8044                capabilities: Vec::new(),
8045            },
8046            RefAdvertisement {
8047                oid: next.clone(),
8048                name: "refs/heads/tmp".into(),
8049                capabilities: Vec::new(),
8050            },
8051        ];
8052        let refspecs = vec![
8053            parse_refspec("+refs/heads/*:refs/remotes/origin/*")
8054                .expect("test operation should succeed"),
8055            parse_refspec("^refs/heads/tmp").expect("test operation should succeed"),
8056        ];
8057        assert_eq!(
8058            plan_fetch_ref_updates(&refs, &refspecs, false).expect("test operation should succeed"),
8059            vec![FetchRefUpdate {
8060                src: "refs/heads/main".into(),
8061                dst: Some("refs/remotes/origin/main".into()),
8062                oid: main,
8063                not_for_merge: false,
8064                force: true,
8065            }]
8066        );
8067    }
8068
8069    #[test]
8070    fn fetch_planner_autofollows_tags_and_builds_fetch_head_records() {
8071        let commit = ObjectId::from_hex(
8072            ObjectFormat::Sha1,
8073            "1111111111111111111111111111111111111111",
8074        )
8075        .expect("test operation should succeed");
8076        let refs = vec![
8077            RefAdvertisement {
8078                oid: commit.clone(),
8079                name: "refs/heads/main".into(),
8080                capabilities: Vec::new(),
8081            },
8082            RefAdvertisement {
8083                oid: commit.clone(),
8084                name: "refs/tags/v1".into(),
8085                capabilities: Vec::new(),
8086            },
8087        ];
8088        let refspecs = vec![
8089            parse_refspec("refs/heads/main:refs/heads/main")
8090                .expect("test operation should succeed"),
8091        ];
8092        let updates =
8093            plan_fetch_ref_updates(&refs, &refspecs, true).expect("test operation should succeed");
8094        assert_eq!(
8095            updates,
8096            vec![
8097                FetchRefUpdate {
8098                    src: "refs/heads/main".into(),
8099                    dst: Some("refs/heads/main".into()),
8100                    oid: commit.clone(),
8101                    not_for_merge: false,
8102                    force: false,
8103                },
8104                FetchRefUpdate {
8105                    src: "refs/tags/v1".into(),
8106                    dst: Some("refs/tags/v1".into()),
8107                    oid: commit.clone(),
8108                    not_for_merge: true,
8109                    force: false,
8110                },
8111            ]
8112        );
8113        assert_eq!(
8114            fetch_ref_updates_to_fetch_head(&updates, "../bundle.bdl")
8115                .expect("test operation should succeed"),
8116            vec![
8117                FetchHeadRecord {
8118                    oid: commit.clone(),
8119                    not_for_merge: false,
8120                    description: "branch 'main' of ../bundle.bdl".into(),
8121                },
8122                FetchHeadRecord {
8123                    oid: commit,
8124                    not_for_merge: true,
8125                    description: "tag 'v1' of ../bundle.bdl".into(),
8126                },
8127            ]
8128        );
8129    }
8130
8131    #[test]
8132    fn fetch_planner_rejects_missing_or_sourceless_refspecs() {
8133        let refs = vec![RefAdvertisement {
8134            oid: ObjectId::from_hex(
8135                ObjectFormat::Sha1,
8136                "1111111111111111111111111111111111111111",
8137            )
8138            .expect("test operation should succeed"),
8139            name: "refs/heads/main".into(),
8140            capabilities: Vec::new(),
8141        }];
8142        assert!(
8143            plan_fetch_ref_updates(
8144                &refs,
8145                &[parse_refspec("refs/heads/missing").expect("test operation should succeed")],
8146                false
8147            )
8148            .is_err()
8149        );
8150        assert!(
8151            plan_fetch_ref_updates(
8152                &refs,
8153                &[parse_refspec(":refs/heads/main").expect("test operation should succeed")],
8154                false
8155            )
8156            .is_err()
8157        );
8158    }
8159
8160    #[test]
8161    fn fetch_planner_sourceless_positive_refspec_returns_err_not_panic() {
8162        // Regression guard for the sley#7 conversion at the `let Some(src) = ..`
8163        // binding: a non-pattern positive refspec with no source must return an
8164        // error, never panic. Construct the malformed RefSpec directly so the
8165        // test pins the converted guard rather than parse_refspec's behavior.
8166        let refs = vec![RefAdvertisement {
8167            oid: ObjectId::from_hex(
8168                ObjectFormat::Sha1,
8169                "1111111111111111111111111111111111111111",
8170            )
8171            .expect("test operation should succeed"),
8172            name: "refs/heads/main".into(),
8173            capabilities: Vec::new(),
8174        }];
8175        let malformed = RefSpec {
8176            force: false,
8177            negative: false,
8178            src: None,
8179            dst: Some("refs/heads/main".into()),
8180            pattern: false,
8181        };
8182        let result = plan_fetch_ref_updates(&refs, &[malformed], false);
8183        assert!(
8184            result.is_err(),
8185            "sourceless positive refspec must yield Err, got {result:?}"
8186        );
8187    }
8188
8189    #[test]
8190    fn push_planner_builds_create_update_delete_and_matching_commands() {
8191        let old = ObjectId::from_hex(
8192            ObjectFormat::Sha1,
8193            "1111111111111111111111111111111111111111",
8194        )
8195        .expect("test operation should succeed");
8196        let new = ObjectId::from_hex(
8197            ObjectFormat::Sha1,
8198            "2222222222222222222222222222222222222222",
8199        )
8200        .expect("test operation should succeed");
8201        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
8202        let local_refs = vec![
8203            PushSourceRef {
8204                oid: new.clone(),
8205                name: "refs/heads/main".into(),
8206            },
8207            PushSourceRef {
8208                oid: new.clone(),
8209                name: "refs/heads/new".into(),
8210            },
8211        ];
8212        let remote_refs = vec![
8213            RefAdvertisement {
8214                oid: old.clone(),
8215                name: "refs/heads/main".into(),
8216                capabilities: Vec::new(),
8217            },
8218            RefAdvertisement {
8219                oid: old.clone(),
8220                name: "refs/heads/old".into(),
8221                capabilities: Vec::new(),
8222            },
8223        ];
8224
8225        assert_eq!(
8226            plan_push_commands(
8227                ObjectFormat::Sha1,
8228                &local_refs,
8229                &remote_refs,
8230                &[parse_refspec("refs/heads/main").expect("test operation should succeed")],
8231            )
8232            .expect("test operation should succeed"),
8233            vec![ReceivePackCommand {
8234                old_id: old.clone(),
8235                new_id: new.clone(),
8236                name: "refs/heads/main".into(),
8237            }]
8238        );
8239        assert_eq!(
8240            plan_push_commands(
8241                ObjectFormat::Sha1,
8242                &local_refs,
8243                &remote_refs,
8244                &[parse_refspec("refs/heads/new:refs/heads/new")
8245                    .expect("test operation should succeed")],
8246            )
8247            .expect("test operation should succeed"),
8248            vec![ReceivePackCommand {
8249                old_id: zero.clone(),
8250                new_id: new.clone(),
8251                name: "refs/heads/new".into(),
8252            }]
8253        );
8254        assert_eq!(
8255            plan_push_commands(
8256                ObjectFormat::Sha1,
8257                &local_refs,
8258                &remote_refs,
8259                &[parse_refspec(":refs/heads/old").expect("test operation should succeed")],
8260            )
8261            .expect("test operation should succeed"),
8262            vec![ReceivePackCommand {
8263                old_id: old.clone(),
8264                new_id: zero,
8265                name: "refs/heads/old".into(),
8266            }]
8267        );
8268        assert_eq!(
8269            plan_push_commands(
8270                ObjectFormat::Sha1,
8271                &local_refs,
8272                &remote_refs,
8273                &[parse_refspec(":").expect("test operation should succeed")],
8274            )
8275            .expect("test operation should succeed"),
8276            vec![ReceivePackCommand {
8277                old_id: old,
8278                new_id: new,
8279                name: "refs/heads/main".into(),
8280            }]
8281        );
8282    }
8283
8284    #[test]
8285    fn push_planner_builds_wildcard_commands_and_rejects_bad_refspecs() {
8286        let new = ObjectId::from_hex(
8287            ObjectFormat::Sha1,
8288            "2222222222222222222222222222222222222222",
8289        )
8290        .expect("test operation should succeed");
8291        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
8292        let local_refs = vec![PushSourceRef {
8293            oid: new.clone(),
8294            name: "refs/heads/topic".into(),
8295        }];
8296        let commands = plan_push_commands(
8297            ObjectFormat::Sha1,
8298            &local_refs,
8299            &[],
8300            &[parse_refspec("refs/heads/*:refs/heads/review/*")
8301                .expect("test operation should succeed")],
8302        )
8303        .expect("test operation should succeed");
8304        assert_eq!(
8305            commands,
8306            vec![ReceivePackCommand {
8307                old_id: zero,
8308                new_id: new,
8309                name: "refs/heads/review/topic".into(),
8310            }]
8311        );
8312        assert!(
8313            plan_push_commands(
8314                ObjectFormat::Sha1,
8315                &local_refs,
8316                &[],
8317                &[parse_refspec("^refs/heads/topic").expect("test operation should succeed")],
8318            )
8319            .is_err()
8320        );
8321        assert_eq!(
8322            plan_push_commands(
8323                ObjectFormat::Sha1,
8324                &local_refs,
8325                &[],
8326                &[parse_refspec(":refs/heads/missing").expect("test operation should succeed")],
8327            )
8328            .expect("missing deletes are sent as zero-to-zero commands"),
8329            vec![ReceivePackCommand {
8330                old_id: zero,
8331                new_id: zero,
8332                name: "refs/heads/missing".into(),
8333            }]
8334        );
8335    }
8336
8337    #[test]
8338    fn receive_pack_push_request_builder_negotiates_capabilities() {
8339        let old_id = ObjectId::from_hex(
8340            ObjectFormat::Sha1,
8341            "1111111111111111111111111111111111111111",
8342        )
8343        .expect("test operation should succeed");
8344        let new_id = ObjectId::from_hex(
8345            ObjectFormat::Sha1,
8346            "2222222222222222222222222222222222222222",
8347        )
8348        .expect("test operation should succeed");
8349        let features = ReceivePackFeatures {
8350            report_status_v2: true,
8351            atomic: true,
8352            ofs_delta: true,
8353            push_options: true,
8354            side_band_64k: true,
8355            quiet: true,
8356            object_format: Some(ObjectFormat::Sha1),
8357            ..ReceivePackFeatures::default()
8358        };
8359        let request = build_receive_pack_push_request(
8360            &features,
8361            vec![ReceivePackCommand {
8362                old_id,
8363                new_id,
8364                name: "refs/heads/main".into(),
8365            }],
8366            b"PACKdata".to_vec(),
8367            ReceivePackPushRequestOptions {
8368                report_status_v2: true,
8369                atomic: true,
8370                ofs_delta: true,
8371                side_band_64k: true,
8372                quiet: true,
8373                agent: Some("sley/0".into()),
8374                object_format: Some(ObjectFormat::Sha1),
8375                push_options: vec!["ci.skip".into()],
8376                ..ReceivePackPushRequestOptions::default()
8377            },
8378        )
8379        .expect("test operation should succeed");
8380        assert_eq!(
8381            request.commands.capabilities,
8382            vec![
8383                Capability {
8384                    name: "report-status-v2".into(),
8385                    value: None,
8386                },
8387                Capability {
8388                    name: "atomic".into(),
8389                    value: None,
8390                },
8391                Capability {
8392                    name: "ofs-delta".into(),
8393                    value: None,
8394                },
8395                Capability {
8396                    name: "side-band-64k".into(),
8397                    value: None,
8398                },
8399                Capability {
8400                    name: "quiet".into(),
8401                    value: None,
8402                },
8403                Capability {
8404                    name: "agent".into(),
8405                    value: Some("sley/0".into()),
8406                },
8407                Capability {
8408                    name: "object-format".into(),
8409                    value: Some("sha1".into()),
8410                },
8411                Capability {
8412                    name: "push-options".into(),
8413                    value: None,
8414                },
8415            ]
8416        );
8417        assert_eq!(request.push_options, Some(vec!["ci.skip".into()]));
8418        validate_receive_pack_push_request_features(&features, &request)
8419            .expect("test operation should succeed");
8420    }
8421
8422    #[test]
8423    fn receive_pack_push_request_builder_handles_delete_only_and_rejects_unadvertised() {
8424        let old_id = ObjectId::from_hex(
8425            ObjectFormat::Sha1,
8426            "1111111111111111111111111111111111111111",
8427        )
8428        .expect("test operation should succeed");
8429        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
8430        let features = ReceivePackFeatures {
8431            delete_refs: true,
8432            ..ReceivePackFeatures::default()
8433        };
8434        let request = build_receive_pack_push_request(
8435            &features,
8436            vec![ReceivePackCommand {
8437                old_id,
8438                new_id: zero,
8439                name: "refs/heads/old".into(),
8440            }],
8441            Vec::new(),
8442            ReceivePackPushRequestOptions::default(),
8443        )
8444        .expect("test operation should succeed");
8445        assert_eq!(
8446            request.commands.capabilities,
8447            vec![Capability {
8448                name: "delete-refs".into(),
8449                value: None,
8450            }]
8451        );
8452        assert!(request.packfile.is_empty());
8453
8454        assert!(
8455            build_receive_pack_push_request(
8456                &ReceivePackFeatures::default(),
8457                request.commands.commands.clone(),
8458                Vec::new(),
8459                ReceivePackPushRequestOptions::default(),
8460            )
8461            .is_err()
8462        );
8463        assert!(
8464            build_receive_pack_push_request(
8465                &features,
8466                request.commands.commands,
8467                b"PACK".to_vec(),
8468                ReceivePackPushRequestOptions::default(),
8469            )
8470            .is_err()
8471        );
8472        assert!(
8473            build_receive_pack_push_request(
8474                &features,
8475                Vec::new(),
8476                Vec::new(),
8477                ReceivePackPushRequestOptions {
8478                    push_options: vec!["ci.skip".into()],
8479                    ..ReceivePackPushRequestOptions::default()
8480                },
8481            )
8482            .is_err()
8483        );
8484    }
8485
8486    #[test]
8487    fn smart_http_helpers_build_paths_and_content_types() {
8488        let sha1 = ObjectId::from_hex(
8489            ObjectFormat::Sha1,
8490            "1111111111111111111111111111111111111111",
8491        )
8492        .expect("test operation should succeed");
8493        let sha256 = ObjectId::from_hex(
8494            ObjectFormat::Sha256,
8495            "2222222222222222222222222222222222222222222222222222222222222222",
8496        )
8497        .expect("test operation should succeed");
8498        assert_eq!(
8499            smart_http_info_refs_path("/repo.git/", GitService::UploadPack)
8500                .expect("test operation should succeed"),
8501            "/repo.git/info/refs?service=git-upload-pack"
8502        );
8503        assert_eq!(
8504            dumb_http_info_refs_path("/repo.git/").expect("test operation should succeed"),
8505            "/repo.git/info/refs"
8506        );
8507        assert_eq!(
8508            dumb_http_alternates_path("/repo.git").expect("test operation should succeed"),
8509            "/repo.git/objects/info/http-alternates"
8510        );
8511        assert_eq!(
8512            dumb_http_packs_path("/repo.git/").expect("test operation should succeed"),
8513            "/repo.git/objects/info/packs"
8514        );
8515        assert_eq!(
8516            dumb_http_loose_object_path("/repo.git/", &sha1)
8517                .expect("test operation should succeed"),
8518            "/repo.git/objects/11/11111111111111111111111111111111111111"
8519        );
8520        assert_eq!(
8521            dumb_http_loose_object_path("/repo.git/", &sha256)
8522                .expect("test operation should succeed"),
8523            "/repo.git/objects/22/22222222222222222222222222222222222222222222222222222222222222"
8524        );
8525        assert_eq!(
8526            dumb_http_pack_file_path("/repo.git/", &sha1).expect("test operation should succeed"),
8527            "/repo.git/objects/pack/pack-1111111111111111111111111111111111111111.pack"
8528        );
8529        assert_eq!(
8530            dumb_http_pack_index_path("/repo.git/", &sha1).expect("test operation should succeed"),
8531            "/repo.git/objects/pack/pack-1111111111111111111111111111111111111111.idx"
8532        );
8533        assert_eq!(
8534            smart_http_rpc_path("/repo.git", GitService::ReceivePack)
8535                .expect("test operation should succeed"),
8536            "/repo.git/git-receive-pack"
8537        );
8538        assert_eq!(
8539            smart_http_advertisement_content_type(GitService::UploadPack)
8540                .expect("test operation should succeed"),
8541            "application/x-git-upload-pack-advertisement"
8542        );
8543        assert_eq!(
8544            smart_http_rpc_request_content_type(GitService::UploadPack)
8545                .expect("test operation should succeed"),
8546            "application/x-git-upload-pack-request"
8547        );
8548        assert_eq!(
8549            smart_http_rpc_result_content_type(GitService::ReceivePack)
8550                .expect("test operation should succeed"),
8551            "application/x-git-receive-pack-result"
8552        );
8553        assert_eq!(
8554            parse_smart_http_advertisement_content_type(
8555                "Application/X-Git-Upload-Pack-Advertisement"
8556            )
8557            .expect("test operation should succeed"),
8558            GitService::UploadPack
8559        );
8560        assert_eq!(
8561            parse_smart_http_rpc_request_content_type("application/x-git-receive-pack-request")
8562                .expect("test operation should succeed"),
8563            GitService::ReceivePack
8564        );
8565        assert_eq!(
8566            parse_smart_http_rpc_result_content_type("application/x-git-upload-pack-result")
8567                .expect("test operation should succeed"),
8568            GitService::UploadPack
8569        );
8570    }
8571
8572    #[test]
8573    fn smart_http_helpers_reject_invalid_services_paths_and_content_types() {
8574        let oid = ObjectId::from_hex(
8575            ObjectFormat::Sha1,
8576            "1111111111111111111111111111111111111111",
8577        )
8578        .expect("test operation should succeed");
8579        assert!(smart_http_info_refs_path("repo.git", GitService::UploadPack).is_err());
8580        assert!(smart_http_rpc_path("/repo.git?x=1", GitService::UploadPack).is_err());
8581        assert!(dumb_http_info_refs_path("repo.git").is_err());
8582        assert!(dumb_http_alternates_path("/repo.git#fragment").is_err());
8583        assert!(dumb_http_packs_path("/repo.git?query").is_err());
8584        assert!(dumb_http_loose_object_path("repo.git", &oid).is_err());
8585        assert!(dumb_http_pack_file_path("/repo.git#fragment", &oid).is_err());
8586        assert!(dumb_http_pack_index_path("/repo.git?query", &oid).is_err());
8587        assert!(smart_http_info_refs_path("/repo.git", GitService::UploadArchive).is_err());
8588        assert!(smart_http_advertisement_content_type(GitService::UploadArchive).is_err());
8589        assert!(
8590            parse_smart_http_advertisement_content_type(
8591                "application/x-git-upload-archive-advertisement"
8592            )
8593            .is_err()
8594        );
8595        assert!(
8596            parse_smart_http_rpc_request_content_type("application/x-git-upload-pack-result")
8597                .is_err()
8598        );
8599        assert!(
8600            parse_smart_http_rpc_result_content_type(
8601                "application/x-git-receive-pack-result; charset=utf-8"
8602            )
8603            .is_err()
8604        );
8605    }
8606
8607    #[test]
8608    fn sideband_packets_parse_and_encode_channels() {
8609        let payloads = vec![
8610            b"\x01PACK bytes".to_vec(),
8611            b"\x02counting objects\n".to_vec(),
8612            b"\x03fatal error\n".to_vec(),
8613        ];
8614        let packets = parse_sideband_packets(&payloads).expect("test operation should succeed");
8615        assert_eq!(
8616            packets,
8617            vec![
8618                SideBandPacket {
8619                    channel: SideBandChannel::Data,
8620                    data: b"PACK bytes".to_vec(),
8621                },
8622                SideBandPacket {
8623                    channel: SideBandChannel::Progress,
8624                    data: b"counting objects\n".to_vec(),
8625                },
8626                SideBandPacket {
8627                    channel: SideBandChannel::Fatal,
8628                    data: b"fatal error\n".to_vec(),
8629                },
8630            ]
8631        );
8632        assert_eq!(
8633            encode_sideband_packets(&packets).expect("test operation should succeed"),
8634            payloads
8635        );
8636    }
8637
8638    #[test]
8639    fn sideband_stream_parses_encodes_and_demuxes_packets() {
8640        let frames = vec![
8641            PktLineFrame::Data(vec![1, b'P', b'A']),
8642            PktLineFrame::Data(vec![2, b'c', b'o', b'u', b'n', b't', b'\n']),
8643            PktLineFrame::Data(vec![1, b'C', b'K']),
8644            PktLineFrame::Flush,
8645        ];
8646        let packets = parse_sideband_stream(&frames).expect("test operation should succeed");
8647        assert_eq!(
8648            packets,
8649            vec![
8650                SideBandPacket {
8651                    channel: SideBandChannel::Data,
8652                    data: b"PA".to_vec(),
8653                },
8654                SideBandPacket {
8655                    channel: SideBandChannel::Progress,
8656                    data: b"count\n".to_vec(),
8657                },
8658                SideBandPacket {
8659                    channel: SideBandChannel::Data,
8660                    data: b"CK".to_vec(),
8661                },
8662            ]
8663        );
8664        assert_eq!(
8665            encode_sideband_stream(&packets).expect("test operation should succeed"),
8666            frames
8667        );
8668        assert_eq!(
8669            demux_sideband_stream(&frames).expect("test operation should succeed"),
8670            SideBandDemux {
8671                data: b"PACK".to_vec(),
8672                progress: vec![b"count\n".to_vec()],
8673            }
8674        );
8675    }
8676
8677    #[test]
8678    fn sideband_stream_reads_and_writes_until_flush() {
8679        let packets = vec![
8680            SideBandPacket {
8681                channel: SideBandChannel::Data,
8682                data: b"PACK".to_vec(),
8683            },
8684            SideBandPacket {
8685                channel: SideBandChannel::Progress,
8686                data: b"done\n".to_vec(),
8687            },
8688        ];
8689        let mut encoded = Vec::new();
8690        write_sideband_stream(&mut encoded, &packets).expect("test operation should succeed");
8691        encoded.extend_from_slice(b"tail");
8692
8693        let mut input = encoded.as_slice();
8694        assert_eq!(
8695            read_sideband_stream(&mut input).expect("test operation should succeed"),
8696            packets
8697        );
8698        assert_eq!(input, b"tail");
8699
8700        let mut input = encoded.as_slice();
8701        assert_eq!(
8702            read_and_demux_sideband_stream(&mut input).expect("test operation should succeed"),
8703            SideBandDemux {
8704                data: b"PACK".to_vec(),
8705                progress: vec![b"done\n".to_vec()],
8706            }
8707        );
8708        assert_eq!(input, b"tail");
8709    }
8710
8711    #[test]
8712    fn sideband_packets_demux_data_and_progress() {
8713        let payloads = vec![
8714            b"\x01PACK".to_vec(),
8715            b"\x02counting objects\n".to_vec(),
8716            b"\x01 bytes".to_vec(),
8717            b"\x02done\n".to_vec(),
8718        ];
8719        assert_eq!(
8720            parse_and_demux_sideband_packets(&payloads).expect("test operation should succeed"),
8721            SideBandDemux {
8722                data: b"PACK bytes".to_vec(),
8723                progress: vec![b"counting objects\n".to_vec(), b"done\n".to_vec()],
8724            }
8725        );
8726    }
8727
8728    #[test]
8729    fn sideband_packets_reject_bad_channels_and_oversize_payloads() {
8730        assert!(parse_sideband_packet(b"").is_err());
8731        assert!(parse_sideband_packet(b"\x04bad").is_err());
8732        assert!(
8733            parse_sideband_stream(&[PktLineFrame::Data(vec![1, b'P', b'A', b'C', b'K'])]).is_err()
8734        );
8735        assert!(parse_sideband_stream(&[PktLineFrame::Delimiter, PktLineFrame::Flush]).is_err());
8736        assert!(
8737            parse_sideband_stream(&[
8738                PktLineFrame::Data(vec![1, b'P', b'A']),
8739                PktLineFrame::Flush,
8740                PktLineFrame::Data(vec![1, b'C', b'K']),
8741            ])
8742            .is_err()
8743        );
8744        assert!(
8745            parse_sideband_stream(&[
8746                PktLineFrame::Data(vec![1, b'P', b'A']),
8747                PktLineFrame::Data(b"\x04bad".to_vec()),
8748                PktLineFrame::Flush,
8749            ])
8750            .is_err()
8751        );
8752        assert!(
8753            encode_sideband_packet(&SideBandPacket {
8754                channel: SideBandChannel::Data,
8755                data: vec![0; PKT_LINE_MAX_PAYLOAD_LEN],
8756            })
8757            .is_err()
8758        );
8759        assert!(
8760            demux_sideband_packets(&[SideBandPacket {
8761                channel: SideBandChannel::Fatal,
8762                data: b"remote died\n".to_vec(),
8763            }])
8764            .is_err()
8765        );
8766    }
8767
8768    #[test]
8769    fn upload_archive_request_parses_and_encodes_arguments() {
8770        let frames = vec![
8771            PktLineFrame::Data(b"argument --format=tar\n".to_vec()),
8772            PktLineFrame::Data(b"argument HEAD:dir with spaces\n".to_vec()),
8773            PktLineFrame::Flush,
8774        ];
8775        let request = parse_upload_archive_request(&frames).expect("test operation should succeed");
8776        assert_eq!(
8777            request,
8778            UploadArchiveRequest {
8779                arguments: vec!["--format=tar".into(), "HEAD:dir with spaces".into()],
8780            }
8781        );
8782        assert_eq!(
8783            encode_upload_archive_request(&request).expect("test operation should succeed"),
8784            frames
8785        );
8786    }
8787
8788    #[test]
8789    fn upload_archive_request_streams_round_trip() {
8790        let request = UploadArchiveRequest {
8791            arguments: vec!["--prefix=src/".into(), "main".into()],
8792        };
8793        let mut encoded = Vec::new();
8794        write_upload_archive_request(&mut encoded, &request)
8795            .expect("test operation should succeed");
8796        encoded.extend_from_slice(b"tail");
8797
8798        let mut input = encoded.as_slice();
8799        assert_eq!(
8800            read_upload_archive_request(&mut input).expect("test operation should succeed"),
8801            request
8802        );
8803        assert_eq!(input, b"tail");
8804    }
8805
8806    #[test]
8807    fn upload_archive_request_rejects_malformed_streams() {
8808        assert!(parse_upload_archive_request(&[PktLineFrame::Flush]).is_err());
8809        assert!(
8810            parse_upload_archive_request(&[
8811                PktLineFrame::Data(b"--format=tar\n".to_vec()),
8812                PktLineFrame::Flush,
8813            ])
8814            .is_err()
8815        );
8816        assert!(
8817            parse_upload_archive_request(&[
8818                PktLineFrame::Data(b"argument HEAD\n".to_vec()),
8819                PktLineFrame::Delimiter,
8820                PktLineFrame::Flush,
8821            ])
8822            .is_err()
8823        );
8824        assert!(
8825            encode_upload_archive_request(&UploadArchiveRequest {
8826                arguments: vec!["bad\narg".into()],
8827            })
8828            .is_err()
8829        );
8830    }
8831
8832    #[test]
8833    fn upload_archive_response_parses_ack_sideband_and_nack() {
8834        let ack_frames = vec![
8835            PktLineFrame::Data(b"ACK\n".to_vec()),
8836            PktLineFrame::Data(b"\x01tar bytes".to_vec()),
8837            PktLineFrame::Data(b"\x02progress\n".to_vec()),
8838            PktLineFrame::Flush,
8839        ];
8840        let response =
8841            parse_upload_archive_response(&ack_frames).expect("test operation should succeed");
8842        assert_eq!(
8843            response,
8844            UploadArchiveResponse::Ack {
8845                sideband: vec![
8846                    SideBandPacket {
8847                        channel: SideBandChannel::Data,
8848                        data: b"tar bytes".to_vec(),
8849                    },
8850                    SideBandPacket {
8851                        channel: SideBandChannel::Progress,
8852                        data: b"progress\n".to_vec(),
8853                    },
8854                ],
8855            }
8856        );
8857        assert_eq!(
8858            encode_upload_archive_response(&response).expect("test operation should succeed"),
8859            ack_frames
8860        );
8861        assert_eq!(
8862            demux_upload_archive_response(&response).expect("test operation should succeed"),
8863            SideBandDemux {
8864                data: b"tar bytes".to_vec(),
8865                progress: vec![b"progress\n".to_vec()],
8866            }
8867        );
8868
8869        let nack = UploadArchiveResponse::Nack {
8870            message: "unreachable tree".into(),
8871        };
8872        let nack_frames = vec![
8873            PktLineFrame::Data(b"NACK unreachable tree\n".to_vec()),
8874            PktLineFrame::Flush,
8875        ];
8876        assert_eq!(
8877            parse_upload_archive_response(&nack_frames).expect("test operation should succeed"),
8878            nack
8879        );
8880        assert_eq!(
8881            encode_upload_archive_response(&nack).expect("test operation should succeed"),
8882            nack_frames
8883        );
8884        assert!(demux_upload_archive_response(&nack).is_err());
8885    }
8886
8887    #[test]
8888    fn upload_archive_response_streams_round_trip() {
8889        let response = UploadArchiveResponse::Ack {
8890            sideband: vec![SideBandPacket {
8891                channel: SideBandChannel::Data,
8892                data: b"tar bytes".to_vec(),
8893            }],
8894        };
8895        let mut encoded = Vec::new();
8896        write_upload_archive_response(&mut encoded, &response)
8897            .expect("test operation should succeed");
8898        encoded.extend_from_slice(b"tail");
8899
8900        let mut input = encoded.as_slice();
8901        assert_eq!(
8902            read_upload_archive_response(&mut input).expect("test operation should succeed"),
8903            response
8904        );
8905        assert_eq!(input, b"tail");
8906    }
8907
8908    #[test]
8909    fn upload_archive_response_rejects_malformed_streams() {
8910        assert!(parse_upload_archive_response(&[]).is_err());
8911        assert!(
8912            parse_upload_archive_response(&[
8913                PktLineFrame::Data(b"ACK\n".to_vec()),
8914                PktLineFrame::Flush,
8915                PktLineFrame::Data(b"\x01tail".to_vec()),
8916            ])
8917            .is_err()
8918        );
8919        assert!(
8920            parse_upload_archive_response(&[
8921                PktLineFrame::Data(b"NACK\n".to_vec()),
8922                PktLineFrame::Flush,
8923            ])
8924            .is_err()
8925        );
8926        assert!(
8927            parse_upload_archive_response(&[
8928                PktLineFrame::Data(b"NACK denied\n".to_vec()),
8929                PktLineFrame::Data(b"\x02extra\n".to_vec()),
8930                PktLineFrame::Flush,
8931            ])
8932            .is_err()
8933        );
8934        assert!(
8935            encode_upload_archive_response(&UploadArchiveResponse::Nack {
8936                message: "bad\nmessage".into(),
8937            })
8938            .is_err()
8939        );
8940    }
8941
8942    #[test]
8943    fn capabilities_parse_and_encode_tokens() {
8944        let capabilities = parse_capabilities(
8945            b"multi_ack thin-pack agent=git/2.54.0 symref=HEAD:refs/heads/main\n",
8946        )
8947        .expect("test operation should succeed");
8948        assert_eq!(
8949            capabilities,
8950            vec![
8951                Capability {
8952                    name: "multi_ack".into(),
8953                    value: None,
8954                },
8955                Capability {
8956                    name: "thin-pack".into(),
8957                    value: None,
8958                },
8959                Capability {
8960                    name: "agent".into(),
8961                    value: Some("git/2.54.0".into()),
8962                },
8963                Capability {
8964                    name: "symref".into(),
8965                    value: Some("HEAD:refs/heads/main".into()),
8966                },
8967            ]
8968        );
8969        assert_eq!(
8970            encode_capabilities(&capabilities).expect("test operation should succeed"),
8971            b"multi_ack thin-pack agent=git/2.54.0 symref=HEAD:refs/heads/main"
8972        );
8973    }
8974
8975    #[test]
8976    fn capabilities_reject_empty_or_delimited_fields() {
8977        assert!(parse_capabilities(b"multi_ack  thin-pack").is_err());
8978        assert!(parse_capabilities(b"agent=").is_err());
8979        assert!(parse_capabilities(b"symref=HEAD:refs/heads/main\nbad").is_err());
8980        assert!(
8981            encode_capabilities(&[Capability {
8982                name: "bad name".into(),
8983                value: None,
8984            }])
8985            .is_err()
8986        );
8987    }
8988
8989    #[test]
8990    fn protocol_v2_object_format_uses_capability_or_defaults_to_sha1() {
8991        assert_eq!(
8992            protocol_v2_object_format(&[]).expect("test operation should succeed"),
8993            ObjectFormat::Sha1
8994        );
8995        assert_eq!(
8996            protocol_v2_object_format(&[Capability {
8997                name: "object-format".into(),
8998                value: Some("sha256".into()),
8999            }])
9000            .expect("test operation should succeed"),
9001            ObjectFormat::Sha256
9002        );
9003        assert!(
9004            protocol_v2_object_format(&[Capability {
9005                name: "object-format".into(),
9006                value: None,
9007            }])
9008            .is_err()
9009        );
9010        assert!(
9011            protocol_v2_object_format(&[
9012                Capability {
9013                    name: "object-format".into(),
9014                    value: Some("sha1".into()),
9015                },
9016                Capability {
9017                    name: "object-format".into(),
9018                    value: Some("sha256".into()),
9019                },
9020            ])
9021            .is_err()
9022        );
9023        assert!(
9024            protocol_v2_object_format(&[Capability {
9025                name: "object-format".into(),
9026                value: Some("unknown".into()),
9027            }])
9028            .is_err()
9029        );
9030    }
9031
9032    #[test]
9033    fn protocol_v2_command_request_capabilities_validate_against_handshake() {
9034        let handshake = TransportHandshake {
9035            protocol: ProtocolVersion::V2,
9036            capabilities: vec![
9037                Capability {
9038                    name: "fetch".into(),
9039                    value: Some("shallow filter".into()),
9040                },
9041                Capability {
9042                    name: "agent".into(),
9043                    value: Some("sley/0".into()),
9044                },
9045                Capability {
9046                    name: "object-format".into(),
9047                    value: Some("sha1".into()),
9048                },
9049            ],
9050        };
9051        validate_protocol_v2_command_request_capabilities(
9052            &handshake,
9053            &ProtocolV2CommandRequest {
9054                command: "fetch".into(),
9055                capabilities: vec![
9056                    Capability {
9057                        name: "agent".into(),
9058                        value: Some("client/1".into()),
9059                    },
9060                    Capability {
9061                        name: "object-format".into(),
9062                        value: Some("sha1".into()),
9063                    },
9064                ],
9065                arguments: Vec::new(),
9066            },
9067        )
9068        .expect("test operation should succeed");
9069        assert!(
9070            validate_protocol_v2_command_request_capabilities(
9071                &handshake,
9072                &ProtocolV2CommandRequest {
9073                    command: "ls-refs".into(),
9074                    capabilities: Vec::new(),
9075                    arguments: Vec::new(),
9076                },
9077            )
9078            .is_err()
9079        );
9080        assert!(
9081            validate_protocol_v2_command_request_capabilities(
9082                &handshake,
9083                &ProtocolV2CommandRequest {
9084                    command: "fetch".into(),
9085                    capabilities: vec![Capability {
9086                        name: "server-option".into(),
9087                        value: None,
9088                    }],
9089                    arguments: Vec::new(),
9090                },
9091            )
9092            .is_err()
9093        );
9094        assert!(
9095            validate_protocol_v2_command_request_capabilities(
9096                &handshake,
9097                &ProtocolV2CommandRequest {
9098                    command: "fetch".into(),
9099                    capabilities: vec![Capability {
9100                        name: "object-format".into(),
9101                        value: Some("sha256".into()),
9102                    }],
9103                    arguments: Vec::new(),
9104                },
9105            )
9106            .is_err()
9107        );
9108        assert!(
9109            validate_protocol_v2_command_request_capabilities(
9110                &handshake,
9111                &ProtocolV2CommandRequest {
9112                    command: "fetch".into(),
9113                    capabilities: vec![Capability {
9114                        name: "agent".into(),
9115                        value: None,
9116                    }],
9117                    arguments: Vec::new(),
9118                },
9119            )
9120            .is_err()
9121        );
9122    }
9123
9124    #[test]
9125    fn protocol_v2_command_options_parse_and_encode_known_capabilities() {
9126        let capabilities = vec![
9127            Capability {
9128                name: "agent".into(),
9129                value: Some("sley/0".into()),
9130            },
9131            Capability {
9132                name: "object-format".into(),
9133                value: Some("sha256".into()),
9134            },
9135            Capability {
9136                name: "server-option".into(),
9137                value: Some("trace=true".into()),
9138            },
9139            Capability {
9140                name: "server-option".into(),
9141                value: Some("region=west".into()),
9142            },
9143            Capability {
9144                name: "session-id".into(),
9145                value: Some("abc123".into()),
9146            },
9147        ];
9148        let options = parse_protocol_v2_command_options(&capabilities)
9149            .expect("test operation should succeed");
9150        assert_eq!(
9151            options,
9152            ProtocolV2CommandOptions {
9153                agent: Some("sley/0".into()),
9154                object_format: Some(ObjectFormat::Sha256),
9155                server_options: vec!["trace=true".into(), "region=west".into()],
9156                extra: vec![Capability {
9157                    name: "session-id".into(),
9158                    value: Some("abc123".into()),
9159                }],
9160            }
9161        );
9162        assert_eq!(
9163            encode_protocol_v2_command_options(&options).expect("test operation should succeed"),
9164            capabilities
9165        );
9166    }
9167
9168    #[test]
9169    fn protocol_v2_command_options_reject_malformed_known_capabilities() {
9170        assert!(
9171            parse_protocol_v2_command_options(&[
9172                Capability {
9173                    name: "agent".into(),
9174                    value: Some("sley/0".into()),
9175                },
9176                Capability {
9177                    name: "agent".into(),
9178                    value: Some("sley/1".into()),
9179                },
9180            ])
9181            .is_err()
9182        );
9183        assert!(
9184            parse_protocol_v2_command_options(&[Capability {
9185                name: "object-format".into(),
9186                value: Some("sha512".into()),
9187            }])
9188            .is_err()
9189        );
9190        assert!(
9191            parse_protocol_v2_command_options(&[Capability {
9192                name: "server-option".into(),
9193                value: None,
9194            }])
9195            .is_err()
9196        );
9197        assert!(
9198            encode_protocol_v2_command_options(&ProtocolV2CommandOptions {
9199                extra: vec![Capability {
9200                    name: "server-option".into(),
9201                    value: Some("trace=true".into()),
9202                }],
9203                ..ProtocolV2CommandOptions::default()
9204            })
9205            .is_err()
9206        );
9207    }
9208
9209    #[test]
9210    fn protocol_v2_ls_refs_features_parse_and_encode_advertisement() {
9211        let capabilities = vec![Capability {
9212            name: "ls-refs".into(),
9213            value: Some("unborn custom".into()),
9214        }];
9215        let features = parse_protocol_v2_ls_refs_features(&capabilities)
9216            .expect("test operation should succeed")
9217            .expect("test operation should succeed");
9218        assert_eq!(
9219            features,
9220            ProtocolV2LsRefsFeatures {
9221                unborn: true,
9222                unknown: vec!["custom".into()],
9223            }
9224        );
9225        assert_eq!(
9226            encode_protocol_v2_ls_refs_capability(&features)
9227                .expect("test operation should succeed"),
9228            capabilities[0]
9229        );
9230        assert_eq!(
9231            parse_protocol_v2_ls_refs_features(&[Capability {
9232                name: "ls-refs".into(),
9233                value: None,
9234            }])
9235            .expect("test operation should succeed")
9236            .expect("test operation should succeed"),
9237            ProtocolV2LsRefsFeatures::default()
9238        );
9239        assert!(
9240            parse_protocol_v2_ls_refs_features(&[Capability {
9241                name: "fetch".into(),
9242                value: Some("filter".into()),
9243            }])
9244            .expect("test operation should succeed")
9245            .is_none()
9246        );
9247    }
9248
9249    #[test]
9250    fn protocol_v2_ls_refs_features_reject_malformed_advertisements() {
9251        assert!(
9252            parse_protocol_v2_ls_refs_features(&[
9253                Capability {
9254                    name: "ls-refs".into(),
9255                    value: None,
9256                },
9257                Capability {
9258                    name: "ls-refs".into(),
9259                    value: None,
9260                },
9261            ])
9262            .is_err()
9263        );
9264        assert!(
9265            parse_protocol_v2_ls_refs_features(&[Capability {
9266                name: "ls-refs".into(),
9267                value: Some("unborn  custom".into()),
9268            }])
9269            .is_err()
9270        );
9271        assert!(
9272            encode_protocol_v2_ls_refs_capability(&ProtocolV2LsRefsFeatures {
9273                unknown: vec!["unborn".into()],
9274                ..ProtocolV2LsRefsFeatures::default()
9275            })
9276            .is_err()
9277        );
9278    }
9279
9280    #[test]
9281    fn protocol_v2_ls_refs_command_request_validates_unborn_feature() {
9282        let handshake = TransportHandshake {
9283            protocol: ProtocolVersion::V2,
9284            capabilities: vec![Capability {
9285                name: "ls-refs".into(),
9286                value: Some("unborn".into()),
9287            }],
9288        };
9289        let request = ProtocolV2CommandRequest {
9290            command: "ls-refs".into(),
9291            capabilities: Vec::new(),
9292            arguments: vec![b"unborn".to_vec(), b"ref-prefix HEAD".to_vec()],
9293        };
9294        let parsed = validate_protocol_v2_ls_refs_command_request(&handshake, &request)
9295            .expect("test operation should succeed");
9296        assert!(parsed.unborn);
9297        assert_eq!(parsed.ref_prefixes, vec!["HEAD"]);
9298
9299        let blocked = TransportHandshake {
9300            protocol: ProtocolVersion::V2,
9301            capabilities: vec![Capability {
9302                name: "ls-refs".into(),
9303                value: None,
9304            }],
9305        };
9306        assert!(validate_protocol_v2_ls_refs_command_request(&blocked, &request).is_err());
9307    }
9308
9309    #[test]
9310    fn protocol_v2_fetch_features_parse_and_encode_advertisement() {
9311        let capabilities = vec![Capability {
9312            name: "fetch".into(),
9313            value: Some(
9314                "shallow wait-for-done filter ref-in-want sideband-all packfile-uris custom".into(),
9315            ),
9316        }];
9317        let features = parse_protocol_v2_fetch_features(&capabilities)
9318            .expect("test operation should succeed")
9319            .expect("test operation should succeed");
9320        assert_eq!(
9321            features,
9322            ProtocolV2FetchFeatures {
9323                shallow: true,
9324                wait_for_done: true,
9325                filter: true,
9326                ref_in_want: true,
9327                sideband_all: true,
9328                packfile_uris: true,
9329                unknown: vec!["custom".into()],
9330            }
9331        );
9332        assert_eq!(
9333            encode_protocol_v2_fetch_capability(&features).expect("test operation should succeed"),
9334            capabilities[0]
9335        );
9336        assert_eq!(
9337            parse_protocol_v2_fetch_features(&[Capability {
9338                name: "fetch".into(),
9339                value: None,
9340            }])
9341            .expect("test operation should succeed")
9342            .expect("test operation should succeed"),
9343            ProtocolV2FetchFeatures::default()
9344        );
9345        assert!(
9346            parse_protocol_v2_fetch_features(&[])
9347                .expect("test operation should succeed")
9348                .is_none()
9349        );
9350    }
9351
9352    #[test]
9353    fn protocol_v2_fetch_features_reject_malformed_advertisements() {
9354        assert!(
9355            parse_protocol_v2_fetch_features(&[
9356                Capability {
9357                    name: "fetch".into(),
9358                    value: None,
9359                },
9360                Capability {
9361                    name: "fetch".into(),
9362                    value: None,
9363                },
9364            ])
9365            .is_err()
9366        );
9367        assert!(
9368            parse_protocol_v2_fetch_features(&[Capability {
9369                name: "fetch".into(),
9370                value: Some("filter  shallow".into()),
9371            }])
9372            .is_err()
9373        );
9374        assert!(
9375            encode_protocol_v2_fetch_capability(&ProtocolV2FetchFeatures {
9376                unknown: vec!["filter".into()],
9377                ..ProtocolV2FetchFeatures::default()
9378            })
9379            .is_err()
9380        );
9381    }
9382
9383    #[test]
9384    fn protocol_v2_fetch_request_features_validate_feature_gated_arguments() {
9385        let features = ProtocolV2FetchFeatures {
9386            shallow: true,
9387            wait_for_done: true,
9388            filter: true,
9389            ref_in_want: true,
9390            sideband_all: true,
9391            packfile_uris: true,
9392            unknown: Vec::new(),
9393        };
9394        validate_protocol_v2_fetch_request_features(
9395            &features,
9396            &ProtocolV2FetchRequest {
9397                want_refs: vec!["refs/heads/main".into()],
9398                shallow: vec![
9399                    ObjectId::from_hex(
9400                        ObjectFormat::Sha1,
9401                        "1111111111111111111111111111111111111111",
9402                    )
9403                    .expect("test operation should succeed"),
9404                ],
9405                deepen: Some(1),
9406                filter: Some("blob:none".into()),
9407                packfile_uris: Some("https".into()),
9408                sideband_all: true,
9409                wait_for_done: true,
9410                ..ProtocolV2FetchRequest::default()
9411            },
9412        )
9413        .expect("test operation should succeed");
9414
9415        let request = ProtocolV2FetchRequest {
9416            want_refs: vec!["refs/heads/main".into()],
9417            filter: Some("blob:none".into()),
9418            sideband_all: true,
9419            ..ProtocolV2FetchRequest::default()
9420        };
9421        assert!(
9422            validate_protocol_v2_fetch_request_features(
9423                &ProtocolV2FetchFeatures::default(),
9424                &request,
9425            )
9426            .is_err()
9427        );
9428        assert!(
9429            validate_protocol_v2_fetch_request_features(
9430                &ProtocolV2FetchFeatures {
9431                    ref_in_want: true,
9432                    filter: true,
9433                    ..ProtocolV2FetchFeatures::default()
9434                },
9435                &request,
9436            )
9437            .is_err()
9438        );
9439    }
9440
9441    #[test]
9442    fn protocol_v2_fetch_command_request_validates_against_handshake_features() {
9443        let handshake = TransportHandshake {
9444            protocol: ProtocolVersion::V2,
9445            capabilities: vec![
9446                Capability {
9447                    name: "fetch".into(),
9448                    value: Some("filter ref-in-want".into()),
9449                },
9450                Capability {
9451                    name: "agent".into(),
9452                    value: Some("sley/0".into()),
9453                },
9454            ],
9455        };
9456        let request = ProtocolV2CommandRequest {
9457            command: "fetch".into(),
9458            capabilities: vec![Capability {
9459                name: "agent".into(),
9460                value: Some("client/1".into()),
9461            }],
9462            arguments: vec![
9463                b"want-ref refs/heads/main".to_vec(),
9464                b"filter blob:none".to_vec(),
9465            ],
9466        };
9467        let fetch =
9468            validate_protocol_v2_fetch_command_request(&handshake, ObjectFormat::Sha1, &request)
9469                .expect("test operation should succeed");
9470        assert_eq!(fetch.want_refs, vec!["refs/heads/main"]);
9471        assert_eq!(fetch.filter.as_deref(), Some("blob:none"));
9472
9473        let mut bad = request.clone();
9474        bad.arguments.push(b"sideband-all".to_vec());
9475        assert!(
9476            validate_protocol_v2_fetch_command_request(&handshake, ObjectFormat::Sha1, &bad)
9477                .is_err()
9478        );
9479    }
9480
9481    #[test]
9482    fn protocol_v2_object_info_request_parses_encodes_and_validates() {
9483        let oid = ObjectId::from_hex(
9484            ObjectFormat::Sha1,
9485            "1111111111111111111111111111111111111111",
9486        )
9487        .expect("test operation should succeed");
9488        let request = ProtocolV2CommandRequest {
9489            command: "object-info".into(),
9490            capabilities: Vec::new(),
9491            arguments: vec![
9492                b"size".to_vec(),
9493                b"oid 1111111111111111111111111111111111111111".to_vec(),
9494            ],
9495        };
9496        let parsed =
9497            ProtocolV2ObjectInfoRequest::from_command_request(ObjectFormat::Sha1, &request)
9498                .expect("test operation should succeed");
9499        assert_eq!(
9500            parsed,
9501            ProtocolV2ObjectInfoRequest {
9502                size: true,
9503                oids: vec![oid],
9504            }
9505        );
9506        assert_eq!(
9507            parsed
9508                .to_command_request()
9509                .expect("test operation should succeed"),
9510            request
9511        );
9512
9513        let handshake = TransportHandshake {
9514            protocol: ProtocolVersion::V2,
9515            capabilities: vec![Capability {
9516                name: "object-info".into(),
9517                value: None,
9518            }],
9519        };
9520        assert_eq!(
9521            validate_protocol_v2_object_info_command_request(
9522                &handshake,
9523                ObjectFormat::Sha1,
9524                &request,
9525            )
9526            .expect("test operation should succeed"),
9527            parsed
9528        );
9529    }
9530
9531    #[test]
9532    fn protocol_v2_object_info_request_streams_round_trip() {
9533        let request = ProtocolV2ObjectInfoRequest {
9534            size: true,
9535            oids: vec![
9536                ObjectId::from_hex(
9537                    ObjectFormat::Sha1,
9538                    "1111111111111111111111111111111111111111",
9539                )
9540                .expect("test operation should succeed"),
9541            ],
9542        };
9543        let mut encoded = Vec::new();
9544        write_protocol_v2_object_info_request(&mut encoded, &request)
9545            .expect("test operation should succeed");
9546        encoded.extend_from_slice(b"tail");
9547
9548        let mut input = encoded.as_slice();
9549        assert_eq!(
9550            read_protocol_v2_object_info_request(ObjectFormat::Sha1, &mut input)
9551                .expect("test operation should succeed"),
9552            request
9553        );
9554        assert_eq!(input, b"tail");
9555    }
9556
9557    #[test]
9558    fn protocol_v2_object_info_request_rejects_malformed_arguments() {
9559        assert!(
9560            ProtocolV2ObjectInfoRequest::from_command_request(
9561                ObjectFormat::Sha1,
9562                &ProtocolV2CommandRequest {
9563                    command: "object-info".into(),
9564                    capabilities: Vec::new(),
9565                    arguments: vec![b"oid 1111111111111111111111111111111111111111".to_vec()],
9566                },
9567            )
9568            .is_err()
9569        );
9570        assert!(
9571            ProtocolV2ObjectInfoRequest::from_command_request(
9572                ObjectFormat::Sha1,
9573                &ProtocolV2CommandRequest {
9574                    command: "object-info".into(),
9575                    capabilities: Vec::new(),
9576                    arguments: vec![b"size".to_vec(), b"size".to_vec()],
9577                },
9578            )
9579            .is_err()
9580        );
9581        assert!(
9582            ProtocolV2ObjectInfoRequest::from_command_request(
9583                ObjectFormat::Sha1,
9584                &ProtocolV2CommandRequest {
9585                    command: "object-info".into(),
9586                    capabilities: Vec::new(),
9587                    arguments: vec![b"size".to_vec()],
9588                },
9589            )
9590            .is_err()
9591        );
9592        assert!(
9593            ProtocolV2ObjectInfoRequest::from_command_request(
9594                ObjectFormat::Sha1,
9595                &ProtocolV2CommandRequest {
9596                    command: "object-info".into(),
9597                    capabilities: Vec::new(),
9598                    arguments: vec![b"size".to_vec(), b"oid not-an-oid".to_vec()],
9599                },
9600            )
9601            .is_err()
9602        );
9603        assert!(
9604            validate_protocol_v2_object_info_command_request(
9605                &TransportHandshake {
9606                    protocol: ProtocolVersion::V2,
9607                    capabilities: Vec::new(),
9608                },
9609                ObjectFormat::Sha1,
9610                &ProtocolV2CommandRequest {
9611                    command: "object-info".into(),
9612                    capabilities: Vec::new(),
9613                    arguments: vec![
9614                        b"size".to_vec(),
9615                        b"oid 1111111111111111111111111111111111111111".to_vec(),
9616                    ],
9617                },
9618            )
9619            .is_err()
9620        );
9621    }
9622
9623    #[test]
9624    fn protocol_v2_command_request_classifies_known_and_unknown_commands() {
9625        let handshake = TransportHandshake {
9626            protocol: ProtocolVersion::V2,
9627            capabilities: vec![
9628                Capability {
9629                    name: "ls-refs".into(),
9630                    value: Some("unborn".into()),
9631                },
9632                Capability {
9633                    name: "fetch".into(),
9634                    value: Some("filter ref-in-want".into()),
9635                },
9636                Capability {
9637                    name: "object-info".into(),
9638                    value: None,
9639                },
9640                Capability {
9641                    name: "server-option".into(),
9642                    value: None,
9643                },
9644                Capability {
9645                    name: "server-info".into(),
9646                    value: Some("custom".into()),
9647                },
9648            ],
9649        };
9650        assert_eq!(
9651            classify_protocol_v2_command_request(
9652                &handshake,
9653                ObjectFormat::Sha1,
9654                &ProtocolV2CommandRequest {
9655                    command: "ls-refs".into(),
9656                    capabilities: Vec::new(),
9657                    arguments: vec![b"unborn".to_vec()],
9658                },
9659            )
9660            .expect("test operation should succeed"),
9661            ProtocolV2Command::LsRefs(ProtocolV2LsRefsRequest {
9662                unborn: true,
9663                ..ProtocolV2LsRefsRequest::default()
9664            })
9665        );
9666        assert_eq!(
9667            classify_protocol_v2_command_request(
9668                &handshake,
9669                ObjectFormat::Sha1,
9670                &ProtocolV2CommandRequest {
9671                    command: "fetch".into(),
9672                    capabilities: Vec::new(),
9673                    arguments: vec![
9674                        b"want-ref refs/heads/main".to_vec(),
9675                        b"filter blob:none".to_vec(),
9676                    ],
9677                },
9678            )
9679            .expect("test operation should succeed"),
9680            ProtocolV2Command::Fetch(ProtocolV2FetchRequest {
9681                want_refs: vec!["refs/heads/main".into()],
9682                filter: Some("blob:none".into()),
9683                ..ProtocolV2FetchRequest::default()
9684            })
9685        );
9686        assert_eq!(
9687            classify_protocol_v2_command_request(
9688                &handshake,
9689                ObjectFormat::Sha1,
9690                &ProtocolV2CommandRequest {
9691                    command: "object-info".into(),
9692                    capabilities: Vec::new(),
9693                    arguments: vec![
9694                        b"size".to_vec(),
9695                        b"oid 1111111111111111111111111111111111111111".to_vec(),
9696                    ],
9697                },
9698            )
9699            .expect("test operation should succeed"),
9700            ProtocolV2Command::ObjectInfo(ProtocolV2ObjectInfoRequest {
9701                size: true,
9702                oids: vec![
9703                    ObjectId::from_hex(
9704                        ObjectFormat::Sha1,
9705                        "1111111111111111111111111111111111111111",
9706                    )
9707                    .expect("test operation should succeed")
9708                ],
9709            })
9710        );
9711
9712        let unknown = ProtocolV2CommandRequest {
9713            command: "server-info".into(),
9714            capabilities: vec![Capability {
9715                name: "server-option".into(),
9716                value: Some("trace=true".into()),
9717            }],
9718            arguments: Vec::new(),
9719        };
9720        assert_eq!(
9721            classify_protocol_v2_command_request(&handshake, ObjectFormat::Sha1, &unknown)
9722                .expect("test operation should succeed"),
9723            ProtocolV2Command::Unknown(unknown)
9724        );
9725        assert!(
9726            classify_protocol_v2_command_request(
9727                &handshake,
9728                ObjectFormat::Sha1,
9729                &ProtocolV2CommandRequest {
9730                    command: "not-advertised".into(),
9731                    capabilities: Vec::new(),
9732                    arguments: Vec::new(),
9733                },
9734            )
9735            .is_err()
9736        );
9737    }
9738
9739    #[test]
9740    fn protocol_v2_session_request_classifies_streamed_command_and_done() {
9741        let handshake = TransportHandshake {
9742            protocol: ProtocolVersion::V2,
9743            capabilities: vec![
9744                Capability {
9745                    name: "ls-refs".into(),
9746                    value: Some("unborn".into()),
9747                },
9748                Capability {
9749                    name: "fetch".into(),
9750                    value: Some("filter ref-in-want".into()),
9751                },
9752            ],
9753        };
9754        let command = ProtocolV2Request::Command(ProtocolV2CommandRequest {
9755            command: "ls-refs".into(),
9756            capabilities: Vec::new(),
9757            arguments: vec![b"unborn".to_vec()],
9758        });
9759        assert_eq!(
9760            classify_protocol_v2_request(&handshake, ObjectFormat::Sha1, &command)
9761                .expect("test operation should succeed"),
9762            ProtocolV2SessionRequest::Command(ProtocolV2Command::LsRefs(ProtocolV2LsRefsRequest {
9763                unborn: true,
9764                ..ProtocolV2LsRefsRequest::default()
9765            }))
9766        );
9767        assert_eq!(
9768            classify_protocol_v2_request(&handshake, ObjectFormat::Sha1, &ProtocolV2Request::Done)
9769                .expect("test operation should succeed"),
9770            ProtocolV2SessionRequest::Done
9771        );
9772
9773        let mut encoded = Vec::new();
9774        write_protocol_v2_request(&mut encoded, &command).expect("test operation should succeed");
9775        write_protocol_v2_request(&mut encoded, &ProtocolV2Request::Done)
9776            .expect("test operation should succeed");
9777        encoded.extend_from_slice(b"tail");
9778
9779        let mut input = encoded.as_slice();
9780        assert_eq!(
9781            read_protocol_v2_session_request(&handshake, ObjectFormat::Sha1, &mut input)
9782                .expect("test operation should succeed"),
9783            ProtocolV2SessionRequest::Command(ProtocolV2Command::LsRefs(ProtocolV2LsRefsRequest {
9784                unborn: true,
9785                ..ProtocolV2LsRefsRequest::default()
9786            }))
9787        );
9788        assert_eq!(
9789            read_protocol_v2_session_request(&handshake, ObjectFormat::Sha1, &mut input)
9790                .expect("test operation should succeed"),
9791            ProtocolV2SessionRequest::Done
9792        );
9793        assert_eq!(input, b"tail");
9794    }
9795
9796    #[test]
9797    fn advertised_ref_parses_first_v0_capability_line() {
9798        let payload =
9799            b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 HEAD\0multi_ack symref=HEAD:refs/heads/main\n";
9800        let advertisement = parse_ref_advertisement(ObjectFormat::Sha1, payload)
9801            .expect("test operation should succeed");
9802        assert_eq!(
9803            advertisement.oid,
9804            ObjectId::from_hex(
9805                ObjectFormat::Sha1,
9806                "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"
9807            )
9808            .expect("test operation should succeed")
9809        );
9810        assert_eq!(advertisement.name, "HEAD");
9811        assert_eq!(
9812            advertisement.capabilities,
9813            vec![
9814                Capability {
9815                    name: "multi_ack".into(),
9816                    value: None,
9817                },
9818                Capability {
9819                    name: "symref".into(),
9820                    value: Some("HEAD:refs/heads/main".into()),
9821                },
9822            ]
9823        );
9824    }
9825
9826    #[test]
9827    fn advertised_ref_parses_lines_without_capabilities() {
9828        let advertisement = parse_ref_advertisement(
9829            ObjectFormat::Sha1,
9830            b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n",
9831        )
9832        .expect("test operation should succeed");
9833        assert_eq!(advertisement.name, "refs/heads/main");
9834        assert!(advertisement.capabilities.is_empty());
9835    }
9836
9837    #[test]
9838    fn advertised_ref_rejects_malformed_payloads() {
9839        assert!(
9840            parse_ref_advertisement(ObjectFormat::Sha1, b"not-an-oid refs/heads/main\n").is_err()
9841        );
9842        assert!(
9843            parse_ref_advertisement(
9844                ObjectFormat::Sha1,
9845                b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n"
9846            )
9847            .is_err()
9848        );
9849    }
9850
9851    #[test]
9852    fn advertised_refs_parse_and_encode_stream() {
9853        let main = ObjectId::from_hex(
9854            ObjectFormat::Sha1,
9855            "1111111111111111111111111111111111111111",
9856        )
9857        .expect("test operation should succeed");
9858        let feature = ObjectId::from_hex(
9859            ObjectFormat::Sha1,
9860            "2222222222222222222222222222222222222222",
9861        )
9862        .expect("test operation should succeed");
9863        let frames = vec![
9864            PktLineFrame::Data(
9865                b"1111111111111111111111111111111111111111 HEAD\0multi_ack thin-pack agent=git/2.54.0\n"
9866                    .to_vec(),
9867            ),
9868            PktLineFrame::Data(
9869                b"2222222222222222222222222222222222222222 refs/heads/feature\n".to_vec(),
9870            ),
9871            PktLineFrame::Flush,
9872        ];
9873        let advertisements = parse_ref_advertisements(ObjectFormat::Sha1, &frames)
9874            .expect("test operation should succeed");
9875        assert_eq!(
9876            advertisements,
9877            vec![
9878                RefAdvertisement {
9879                    oid: main,
9880                    name: "HEAD".into(),
9881                    capabilities: vec![
9882                        Capability {
9883                            name: "multi_ack".into(),
9884                            value: None,
9885                        },
9886                        Capability {
9887                            name: "thin-pack".into(),
9888                            value: None,
9889                        },
9890                        Capability {
9891                            name: "agent".into(),
9892                            value: Some("git/2.54.0".into()),
9893                        },
9894                    ],
9895                },
9896                RefAdvertisement {
9897                    oid: feature,
9898                    name: "refs/heads/feature".into(),
9899                    capabilities: Vec::new(),
9900                },
9901            ]
9902        );
9903        assert_eq!(
9904            encode_ref_advertisements(&advertisements).expect("test operation should succeed"),
9905            frames
9906        );
9907        assert_eq!(
9908            parse_ref_advertisements(ObjectFormat::Sha1, &[PktLineFrame::Flush])
9909                .expect("test operation should succeed"),
9910            Vec::<RefAdvertisement>::new()
9911        );
9912    }
9913
9914    #[test]
9915    fn advertised_ref_set_parses_v1_version_refs_and_shallow() {
9916        let main = ObjectId::from_hex(
9917            ObjectFormat::Sha1,
9918            "1111111111111111111111111111111111111111",
9919        )
9920        .expect("test operation should succeed");
9921        let feature = ObjectId::from_hex(
9922            ObjectFormat::Sha1,
9923            "2222222222222222222222222222222222222222",
9924        )
9925        .expect("test operation should succeed");
9926        let shallow = ObjectId::from_hex(
9927            ObjectFormat::Sha1,
9928            "3333333333333333333333333333333333333333",
9929        )
9930        .expect("test operation should succeed");
9931        let frames = vec![
9932            PktLineFrame::Data(b"version 1\n".to_vec()),
9933            PktLineFrame::Data(
9934                b"1111111111111111111111111111111111111111 HEAD\0multi_ack symref=HEAD:refs/heads/main\n"
9935                    .to_vec(),
9936            ),
9937            PktLineFrame::Data(
9938                b"2222222222222222222222222222222222222222 refs/heads/feature\n".to_vec(),
9939            ),
9940            PktLineFrame::Data(b"shallow 3333333333333333333333333333333333333333\n".to_vec()),
9941            PktLineFrame::Flush,
9942        ];
9943
9944        let set = parse_ref_advertisement_set(ObjectFormat::Sha1, &frames)
9945            .expect("test operation should succeed");
9946        assert_eq!(set.protocol, ProtocolVersion::V1);
9947        assert_eq!(set.shallow, vec![shallow]);
9948        assert_eq!(
9949            set.refs,
9950            vec![
9951                RefAdvertisement {
9952                    oid: main,
9953                    name: "HEAD".into(),
9954                    capabilities: vec![
9955                        Capability {
9956                            name: "multi_ack".into(),
9957                            value: None,
9958                        },
9959                        Capability {
9960                            name: "symref".into(),
9961                            value: Some("HEAD:refs/heads/main".into()),
9962                        },
9963                    ],
9964                },
9965                RefAdvertisement {
9966                    oid: feature,
9967                    name: "refs/heads/feature".into(),
9968                    capabilities: Vec::new(),
9969                },
9970            ]
9971        );
9972        assert_eq!(
9973            parse_ref_advertisements(ObjectFormat::Sha1, &frames)
9974                .expect("test operation should succeed"),
9975            set.refs
9976        );
9977        assert_eq!(
9978            encode_ref_advertisement_set(&set).expect("test operation should succeed"),
9979            frames
9980        );
9981    }
9982
9983    #[test]
9984    fn advertised_refs_streams_round_trip() {
9985        let advertisements = vec![RefAdvertisement {
9986            oid: ObjectId::from_hex(
9987                ObjectFormat::Sha1,
9988                "1111111111111111111111111111111111111111",
9989            )
9990            .expect("test operation should succeed"),
9991            name: "HEAD".into(),
9992            capabilities: vec![Capability {
9993                name: "symref".into(),
9994                value: Some("HEAD:refs/heads/main".into()),
9995            }],
9996        }];
9997        let mut encoded = Vec::new();
9998        write_ref_advertisements(&mut encoded, &advertisements)
9999            .expect("test operation should succeed");
10000        encoded.extend_from_slice(b"tail");
10001
10002        let mut input = encoded.as_slice();
10003        assert_eq!(
10004            read_ref_advertisements(ObjectFormat::Sha1, &mut input)
10005                .expect("test operation should succeed"),
10006            advertisements
10007        );
10008        assert_eq!(input, b"tail");
10009    }
10010
10011    #[test]
10012    fn advertised_ref_set_streams_round_trip() {
10013        let set = RefAdvertisementSet {
10014            protocol: ProtocolVersion::V1,
10015            refs: vec![RefAdvertisement {
10016                oid: ObjectId::from_hex(
10017                    ObjectFormat::Sha1,
10018                    "1111111111111111111111111111111111111111",
10019                )
10020                .expect("test operation should succeed"),
10021                name: "HEAD".into(),
10022                capabilities: vec![Capability {
10023                    name: "symref".into(),
10024                    value: Some("HEAD:refs/heads/main".into()),
10025                }],
10026            }],
10027            shallow: vec![
10028                ObjectId::from_hex(
10029                    ObjectFormat::Sha1,
10030                    "2222222222222222222222222222222222222222",
10031                )
10032                .expect("test operation should succeed"),
10033            ],
10034        };
10035        let mut encoded = Vec::new();
10036        write_ref_advertisement_set(&mut encoded, &set).expect("test operation should succeed");
10037        encoded.extend_from_slice(b"tail");
10038
10039        let mut input = encoded.as_slice();
10040        assert_eq!(
10041            read_ref_advertisement_set(ObjectFormat::Sha1, &mut input)
10042                .expect("test operation should succeed"),
10043            set
10044        );
10045        assert_eq!(input, b"tail");
10046    }
10047
10048    #[test]
10049    fn advertised_refs_reject_malformed_streams() {
10050        assert!(
10051            parse_ref_advertisements(
10052                ObjectFormat::Sha1,
10053                &[PktLineFrame::Data(
10054                    b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),
10055                )],
10056            )
10057            .is_err()
10058        );
10059        assert!(
10060            parse_ref_advertisements(
10061                ObjectFormat::Sha1,
10062                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
10063            )
10064            .is_err()
10065        );
10066        assert!(parse_ref_advertisements(
10067            ObjectFormat::Sha1,
10068            &[
10069                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
10070                PktLineFrame::Data(
10071                    b"2222222222222222222222222222222222222222 refs/heads/main\0thin-pack\n"
10072                        .to_vec(),
10073                ),
10074                PktLineFrame::Flush,
10075            ],
10076        )
10077        .is_err());
10078        assert!(parse_ref_advertisement_set(
10079            ObjectFormat::Sha1,
10080            &[
10081                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
10082                PktLineFrame::Data(b"version 1\n".to_vec()),
10083                PktLineFrame::Flush,
10084            ],
10085        )
10086        .is_err());
10087        assert!(
10088            parse_ref_advertisement_set(
10089                ObjectFormat::Sha1,
10090                &[
10091                    PktLineFrame::Data(b"version 2\n".to_vec()),
10092                    PktLineFrame::Flush,
10093                ],
10094            )
10095            .is_err()
10096        );
10097        assert!(
10098            parse_ref_advertisement_set(
10099                ObjectFormat::Sha1,
10100                &[
10101                    PktLineFrame::Data(
10102                        b"shallow 1111111111111111111111111111111111111111\n".to_vec()
10103                    ),
10104                    PktLineFrame::Flush,
10105                ],
10106            )
10107            .is_err()
10108        );
10109        assert!(parse_ref_advertisement_set(
10110            ObjectFormat::Sha1,
10111            &[
10112                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
10113                PktLineFrame::Data(b"shallow not-an-oid\n".to_vec()),
10114                PktLineFrame::Flush,
10115            ],
10116        )
10117        .is_err());
10118        assert!(parse_ref_advertisement_set(
10119            ObjectFormat::Sha1,
10120            &[
10121                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
10122                PktLineFrame::Data(b"shallow 2222222222222222222222222222222222222222\n".to_vec()),
10123                PktLineFrame::Data(
10124                    b"3333333333333333333333333333333333333333 refs/heads/main\n".to_vec(),
10125                ),
10126                PktLineFrame::Flush,
10127            ],
10128        )
10129        .is_err());
10130        assert!(
10131            encode_ref_advertisements(&[
10132                RefAdvertisement {
10133                    oid: ObjectId::from_hex(
10134                        ObjectFormat::Sha1,
10135                        "1111111111111111111111111111111111111111",
10136                    )
10137                    .expect("test operation should succeed"),
10138                    name: "HEAD".into(),
10139                    capabilities: Vec::new(),
10140                },
10141                RefAdvertisement {
10142                    oid: ObjectId::from_hex(
10143                        ObjectFormat::Sha1,
10144                        "2222222222222222222222222222222222222222",
10145                    )
10146                    .expect("test operation should succeed"),
10147                    name: "refs/heads/main".into(),
10148                    capabilities: vec![Capability {
10149                        name: "thin-pack".into(),
10150                        value: None,
10151                    }],
10152                },
10153            ])
10154            .is_err()
10155        );
10156        assert!(
10157            encode_ref_advertisement(&RefAdvertisement {
10158                oid: ObjectId::from_hex(
10159                    ObjectFormat::Sha1,
10160                    "1111111111111111111111111111111111111111",
10161                )
10162                .expect("test operation should succeed"),
10163                name: "bad ref".into(),
10164                capabilities: Vec::new(),
10165            })
10166            .is_err()
10167        );
10168        assert!(
10169            encode_ref_advertisement_set(&RefAdvertisementSet {
10170                protocol: ProtocolVersion::V2,
10171                refs: Vec::new(),
10172                shallow: Vec::new(),
10173            })
10174            .is_err()
10175        );
10176        assert!(
10177            encode_ref_advertisement_set(&RefAdvertisementSet {
10178                protocol: ProtocolVersion::V0,
10179                refs: Vec::new(),
10180                shallow: vec![
10181                    ObjectId::from_hex(
10182                        ObjectFormat::Sha1,
10183                        "1111111111111111111111111111111111111111",
10184                    )
10185                    .expect("test operation should succeed")
10186                ],
10187            })
10188            .is_err()
10189        );
10190    }
10191
10192    #[test]
10193    fn dumb_http_info_refs_parse_and_encode_records() {
10194        let main = ObjectId::from_hex(
10195            ObjectFormat::Sha1,
10196            "1111111111111111111111111111111111111111",
10197        )
10198        .expect("test operation should succeed");
10199        let tag = ObjectId::from_hex(
10200            ObjectFormat::Sha1,
10201            "2222222222222222222222222222222222222222",
10202        )
10203        .expect("test operation should succeed");
10204        let peeled = ObjectId::from_hex(
10205            ObjectFormat::Sha1,
10206            "3333333333333333333333333333333333333333",
10207        )
10208        .expect("test operation should succeed");
10209        let input = b"1111111111111111111111111111111111111111\trefs/heads/main\n2222222222222222222222222222222222222222\trefs/tags/v1.0\n3333333333333333333333333333333333333333\trefs/tags/v1.0^{}\n";
10210
10211        let records = parse_dumb_http_info_refs(ObjectFormat::Sha1, input)
10212            .expect("test operation should succeed");
10213        assert_eq!(
10214            records,
10215            vec![
10216                DumbHttpRefRecord {
10217                    oid: main,
10218                    name: "refs/heads/main".into(),
10219                    peeled: false,
10220                },
10221                DumbHttpRefRecord {
10222                    oid: tag,
10223                    name: "refs/tags/v1.0".into(),
10224                    peeled: false,
10225                },
10226                DumbHttpRefRecord {
10227                    oid: peeled,
10228                    name: "refs/tags/v1.0".into(),
10229                    peeled: true,
10230                },
10231            ]
10232        );
10233        assert_eq!(
10234            encode_dumb_http_info_refs(&records).expect("test operation should succeed"),
10235            input
10236        );
10237        assert_eq!(
10238            parse_dumb_http_info_refs(ObjectFormat::Sha1, b"")
10239                .expect("test operation should succeed"),
10240            Vec::<DumbHttpRefRecord>::new()
10241        );
10242    }
10243
10244    #[test]
10245    fn dumb_http_info_refs_streams_round_trip() {
10246        let records = vec![DumbHttpRefRecord {
10247            oid: ObjectId::from_hex(
10248                ObjectFormat::Sha1,
10249                "1111111111111111111111111111111111111111",
10250            )
10251            .expect("test operation should succeed"),
10252            name: "refs/heads/main".into(),
10253            peeled: false,
10254        }];
10255        let mut encoded = Vec::new();
10256        write_dumb_http_info_refs(&mut encoded, &records).expect("test operation should succeed");
10257        let mut input = encoded.as_slice();
10258        assert_eq!(
10259            read_dumb_http_info_refs(ObjectFormat::Sha1, &mut input)
10260                .expect("test operation should succeed"),
10261            records
10262        );
10263        assert!(input.is_empty());
10264    }
10265
10266    #[test]
10267    fn dumb_http_info_refs_reject_malformed_records() {
10268        assert!(
10269            parse_dumb_http_info_refs(
10270                ObjectFormat::Sha1,
10271                b"1111111111111111111111111111111111111111 refs/heads/main\n",
10272            )
10273            .is_err()
10274        );
10275        assert!(
10276            parse_dumb_http_info_refs(
10277                ObjectFormat::Sha1,
10278                b"1111111111111111111111111111111111111111\trefs/heads/main",
10279            )
10280            .is_err()
10281        );
10282        assert!(
10283            parse_dumb_http_info_refs(ObjectFormat::Sha1, b"not-an-oid\trefs/heads/main\n")
10284                .is_err()
10285        );
10286        assert!(
10287            parse_dumb_http_info_refs(
10288                ObjectFormat::Sha1,
10289                b"1111111111111111111111111111111111111111\tbad ref\n",
10290            )
10291            .is_err()
10292        );
10293        assert!(
10294            encode_dumb_http_info_refs(&[DumbHttpRefRecord {
10295                oid: ObjectId::from_hex(
10296                    ObjectFormat::Sha1,
10297                    "1111111111111111111111111111111111111111",
10298                )
10299                .expect("test operation should succeed"),
10300                name: "refs/tags/v1.0^{}".into(),
10301                peeled: false,
10302            }])
10303            .is_err()
10304        );
10305    }
10306
10307    #[test]
10308    fn dumb_http_alternates_parse_and_encode_locations() {
10309        let input = b"https://example.com/base.git/objects/\n../other.git/objects/\n";
10310        let alternates = parse_dumb_http_alternates(input).expect("test operation should succeed");
10311        assert_eq!(
10312            alternates,
10313            vec![
10314                "https://example.com/base.git/objects/".to_string(),
10315                "../other.git/objects/".to_string(),
10316            ]
10317        );
10318        assert_eq!(
10319            encode_dumb_http_alternates(&alternates).expect("test operation should succeed"),
10320            input
10321        );
10322        assert_eq!(
10323            parse_dumb_http_alternates(b"").expect("test operation should succeed"),
10324            Vec::<String>::new()
10325        );
10326    }
10327
10328    #[test]
10329    fn dumb_http_alternates_streams_round_trip() {
10330        let alternates = vec!["https://example.com/base.git/objects/".to_string()];
10331        let mut encoded = Vec::new();
10332        write_dumb_http_alternates(&mut encoded, &alternates)
10333            .expect("test operation should succeed");
10334        let mut input = encoded.as_slice();
10335        assert_eq!(
10336            read_dumb_http_alternates(&mut input).expect("test operation should succeed"),
10337            alternates
10338        );
10339        assert!(input.is_empty());
10340    }
10341
10342    #[test]
10343    fn dumb_http_alternates_reject_malformed_lines() {
10344        assert!(parse_dumb_http_alternates(b"https://example.com/base.git/objects/").is_err());
10345        assert!(parse_dumb_http_alternates(b"\n").is_err());
10346        assert!(parse_dumb_http_alternates(b"https://example.com/base.git/objects/\r\n").is_err());
10347        assert!(encode_dumb_http_alternates(&["bad\nalternate".to_string()]).is_err());
10348    }
10349
10350    #[test]
10351    fn dumb_http_packs_parse_and_encode_pack_records() {
10352        let first = ObjectId::from_hex(
10353            ObjectFormat::Sha1,
10354            "1111111111111111111111111111111111111111",
10355        )
10356        .expect("test operation should succeed");
10357        let second = ObjectId::from_hex(
10358            ObjectFormat::Sha1,
10359            "2222222222222222222222222222222222222222",
10360        )
10361        .expect("test operation should succeed");
10362        let input = b"P pack-1111111111111111111111111111111111111111.pack\nP pack-2222222222222222222222222222222222222222.pack\n";
10363        let records = parse_dumb_http_packs(ObjectFormat::Sha1, input)
10364            .expect("test operation should succeed");
10365        assert_eq!(
10366            records,
10367            vec![
10368                DumbHttpPackRecord { hash: first },
10369                DumbHttpPackRecord { hash: second },
10370            ]
10371        );
10372        assert_eq!(
10373            encode_dumb_http_packs(&records).expect("test operation should succeed"),
10374            input
10375        );
10376        assert_eq!(
10377            parse_dumb_http_packs(ObjectFormat::Sha1, b"").expect("test operation should succeed"),
10378            Vec::<DumbHttpPackRecord>::new()
10379        );
10380    }
10381
10382    #[test]
10383    fn dumb_http_packs_streams_round_trip() {
10384        let records = vec![DumbHttpPackRecord {
10385            hash: ObjectId::from_hex(
10386                ObjectFormat::Sha1,
10387                "1111111111111111111111111111111111111111",
10388            )
10389            .expect("test operation should succeed"),
10390        }];
10391        let mut encoded = Vec::new();
10392        write_dumb_http_packs(&mut encoded, &records).expect("test operation should succeed");
10393        let mut input = encoded.as_slice();
10394        assert_eq!(
10395            read_dumb_http_packs(ObjectFormat::Sha1, &mut input)
10396                .expect("test operation should succeed"),
10397            records
10398        );
10399        assert!(input.is_empty());
10400    }
10401
10402    #[test]
10403    fn dumb_http_packs_reject_malformed_records() {
10404        assert!(
10405            parse_dumb_http_packs(
10406                ObjectFormat::Sha1,
10407                b"P pack-1111111111111111111111111111111111111111.pack",
10408            )
10409            .is_err()
10410        );
10411        assert!(
10412            parse_dumb_http_packs(
10413                ObjectFormat::Sha1,
10414                b"pack-1111111111111111111111111111111111111111.pack\n",
10415            )
10416            .is_err()
10417        );
10418        assert!(parse_dumb_http_packs(ObjectFormat::Sha1, b"P pack-not-a-hash.pack\n",).is_err());
10419        assert!(
10420            parse_dumb_http_packs(
10421                ObjectFormat::Sha1,
10422                b"P pack-1111111111111111111111111111111111111111.idx\n",
10423            )
10424            .is_err()
10425        );
10426    }
10427
10428    #[test]
10429    fn upload_pack_features_parse_encode_and_validate_request() {
10430        let capabilities = vec![
10431            Capability {
10432                name: "multi_ack".into(),
10433                value: None,
10434            },
10435            Capability {
10436                name: "multi_ack_detailed".into(),
10437                value: None,
10438            },
10439            Capability {
10440                name: "no-done".into(),
10441                value: None,
10442            },
10443            Capability {
10444                name: "thin-pack".into(),
10445                value: None,
10446            },
10447            Capability {
10448                name: "side-band-64k".into(),
10449                value: None,
10450            },
10451            Capability {
10452                name: "ofs-delta".into(),
10453                value: None,
10454            },
10455            Capability {
10456                name: "shallow".into(),
10457                value: None,
10458            },
10459            Capability {
10460                name: "deepen-since".into(),
10461                value: None,
10462            },
10463            Capability {
10464                name: "deepen-not".into(),
10465                value: None,
10466            },
10467            Capability {
10468                name: "include-tag".into(),
10469                value: None,
10470            },
10471            Capability {
10472                name: "no-progress".into(),
10473                value: None,
10474            },
10475            Capability {
10476                name: "filter".into(),
10477                value: None,
10478            },
10479            Capability {
10480                name: "agent".into(),
10481                value: Some("git/2.54.0".into()),
10482            },
10483            Capability {
10484                name: "object-format".into(),
10485                value: Some("sha256".into()),
10486            },
10487            Capability {
10488                name: "symref".into(),
10489                value: Some("HEAD:refs/heads/main".into()),
10490            },
10491            Capability {
10492                name: "custom".into(),
10493                value: Some("value".into()),
10494            },
10495        ];
10496        let features =
10497            parse_upload_pack_features(&capabilities).expect("test operation should succeed");
10498        assert_eq!(
10499            features,
10500            UploadPackFeatures {
10501                multi_ack: true,
10502                multi_ack_detailed: true,
10503                no_done: true,
10504                thin_pack: true,
10505                side_band_64k: true,
10506                ofs_delta: true,
10507                shallow: true,
10508                deepen_since: true,
10509                deepen_not: true,
10510                include_tag: true,
10511                no_progress: true,
10512                filter: true,
10513                agent: Some("git/2.54.0".into()),
10514                object_format: Some(ObjectFormat::Sha256),
10515                symrefs: vec!["HEAD:refs/heads/main".into()],
10516                unknown: vec![Capability {
10517                    name: "custom".into(),
10518                    value: Some("value".into()),
10519                }],
10520                ..UploadPackFeatures::default()
10521            }
10522        );
10523        assert_eq!(
10524            encode_upload_pack_features(&features).expect("test operation should succeed"),
10525            capabilities
10526        );
10527
10528        let request = UploadPackRequest {
10529            wants: vec![
10530                ObjectId::from_hex(
10531                    ObjectFormat::Sha1,
10532                    "1111111111111111111111111111111111111111",
10533                )
10534                .expect("test operation should succeed"),
10535            ],
10536            capabilities: vec![
10537                Capability {
10538                    name: "multi_ack_detailed".into(),
10539                    value: None,
10540                },
10541                Capability {
10542                    name: "thin-pack".into(),
10543                    value: None,
10544                },
10545                Capability {
10546                    name: "side-band-64k".into(),
10547                    value: None,
10548                },
10549                Capability {
10550                    name: "ofs-delta".into(),
10551                    value: None,
10552                },
10553                Capability {
10554                    name: "include-tag".into(),
10555                    value: None,
10556                },
10557                Capability {
10558                    name: "agent".into(),
10559                    value: Some("sley".into()),
10560                },
10561            ],
10562            shallow: vec![
10563                ObjectId::from_hex(
10564                    ObjectFormat::Sha1,
10565                    "2222222222222222222222222222222222222222",
10566                )
10567                .expect("test operation should succeed"),
10568            ],
10569            deepen: Some(5),
10570            deepen_since: Some(1_710_000_000),
10571            deepen_not: vec!["refs/tags/base".into()],
10572            filter: Some("blob:none".into()),
10573        };
10574        validate_upload_pack_request_features(&features, &request)
10575            .expect("test operation should succeed");
10576    }
10577
10578    #[test]
10579    fn upload_pack_features_reject_invalid_requests() {
10580        let want = ObjectId::from_hex(
10581            ObjectFormat::Sha1,
10582            "1111111111111111111111111111111111111111",
10583        )
10584        .expect("test operation should succeed");
10585        let features = UploadPackFeatures {
10586            thin_pack: true,
10587            side_band: true,
10588            ..UploadPackFeatures::default()
10589        };
10590
10591        assert!(
10592            validate_upload_pack_request_features(
10593                &features,
10594                &UploadPackRequest {
10595                    wants: vec![want],
10596                    capabilities: vec![Capability {
10597                        name: "ofs-delta".into(),
10598                        value: None,
10599                    }],
10600                    ..UploadPackRequest::default()
10601                },
10602            )
10603            .is_err()
10604        );
10605        assert!(
10606            validate_upload_pack_request_features(
10607                &features,
10608                &UploadPackRequest {
10609                    wants: vec![want],
10610                    shallow: vec![want],
10611                    ..UploadPackRequest::default()
10612                },
10613            )
10614            .is_err()
10615        );
10616        assert!(
10617            validate_upload_pack_request_features(
10618                &features,
10619                &UploadPackRequest {
10620                    wants: vec![want],
10621                    filter: Some("blob:none".into()),
10622                    ..UploadPackRequest::default()
10623                },
10624            )
10625            .is_err()
10626        );
10627        assert!(
10628            validate_upload_pack_request_features(
10629                &UploadPackFeatures {
10630                    side_band: true,
10631                    side_band_64k: true,
10632                    ..UploadPackFeatures::default()
10633                },
10634                &UploadPackRequest {
10635                    wants: vec![want],
10636                    capabilities: vec![
10637                        Capability {
10638                            name: "side-band".into(),
10639                            value: None,
10640                        },
10641                        Capability {
10642                            name: "side-band-64k".into(),
10643                            value: None,
10644                        },
10645                    ],
10646                    ..UploadPackRequest::default()
10647                },
10648            )
10649            .is_err()
10650        );
10651
10652        assert!(
10653            parse_upload_pack_features(&[
10654                Capability {
10655                    name: "thin-pack".into(),
10656                    value: None,
10657                },
10658                Capability {
10659                    name: "thin-pack".into(),
10660                    value: None,
10661                },
10662            ])
10663            .is_err()
10664        );
10665        assert!(
10666            encode_upload_pack_features(&UploadPackFeatures {
10667                unknown: vec![Capability {
10668                    name: "filter".into(),
10669                    value: None,
10670                }],
10671                ..UploadPackFeatures::default()
10672            })
10673            .is_err()
10674        );
10675    }
10676
10677    #[test]
10678    fn upload_pack_raw_response_builder_filters_unknown_haves_and_builds_pack() {
10679        let want = ObjectId::from_hex(
10680            ObjectFormat::Sha1,
10681            "1111111111111111111111111111111111111111",
10682        )
10683        .expect("test operation should succeed");
10684        let known_have = ObjectId::from_hex(
10685            ObjectFormat::Sha1,
10686            "2222222222222222222222222222222222222222",
10687        )
10688        .expect("test operation should succeed");
10689        let unknown_have = ObjectId::from_hex(
10690            ObjectFormat::Sha1,
10691            "3333333333333333333333333333333333333333",
10692        )
10693        .expect("test operation should succeed");
10694        let existing = std::collections::HashSet::from([want, known_have]);
10695
10696        let response = build_upload_pack_raw_packfile_response(
10697            &UploadPackFeatures::default(),
10698            UploadPackRequest {
10699                wants: vec![want],
10700                ..UploadPackRequest::default()
10701            },
10702            [known_have, unknown_have],
10703            |oid| Ok(existing.contains(oid)),
10704            |wants, haves| {
10705                assert_eq!(wants, vec![want]);
10706                assert_eq!(haves, vec![known_have]);
10707                Ok(Some(b"PACKmock".to_vec()))
10708            },
10709        )
10710        .expect("test operation should succeed");
10711
10712        assert_eq!(
10713            response.acknowledgments,
10714            vec![UploadPackAcknowledgment::Nak]
10715        );
10716        assert_eq!(response.packfile, b"PACKmock");
10717    }
10718
10719    #[test]
10720    fn upload_pack_raw_response_builder_rejects_missing_want_and_empty_pack() {
10721        let want = ObjectId::from_hex(
10722            ObjectFormat::Sha1,
10723            "1111111111111111111111111111111111111111",
10724        )
10725        .expect("test operation should succeed");
10726
10727        assert!(
10728            build_upload_pack_raw_packfile_response(
10729                &UploadPackFeatures::default(),
10730                UploadPackRequest {
10731                    wants: vec![want],
10732                    ..UploadPackRequest::default()
10733                },
10734                Vec::<ObjectId>::new(),
10735                |_| Ok(false),
10736                |_, _| Ok(Some(b"PACKmock".to_vec())),
10737            )
10738            .is_err()
10739        );
10740
10741        assert!(
10742            build_upload_pack_raw_packfile_response(
10743                &UploadPackFeatures::default(),
10744                UploadPackRequest {
10745                    wants: vec![want],
10746                    ..UploadPackRequest::default()
10747                },
10748                Vec::<ObjectId>::new(),
10749                |_| Ok(true),
10750                |_, _| Ok(None),
10751            )
10752            .is_err()
10753        );
10754    }
10755
10756    #[test]
10757    fn upload_pack_request_parses_and_encodes_initial_fetch_request() {
10758        let want = ObjectId::from_hex(
10759            ObjectFormat::Sha1,
10760            "1111111111111111111111111111111111111111",
10761        )
10762        .expect("test operation should succeed");
10763        let second_want = ObjectId::from_hex(
10764            ObjectFormat::Sha1,
10765            "2222222222222222222222222222222222222222",
10766        )
10767        .expect("test operation should succeed");
10768        let shallow = ObjectId::from_hex(
10769            ObjectFormat::Sha1,
10770            "3333333333333333333333333333333333333333",
10771        )
10772        .expect("test operation should succeed");
10773        let frames = vec![
10774            PktLineFrame::Data(
10775                b"want 1111111111111111111111111111111111111111 multi_ack thin-pack agent=git/2.54.0\n"
10776                    .to_vec(),
10777            ),
10778            PktLineFrame::Data(b"want 2222222222222222222222222222222222222222\n".to_vec()),
10779            PktLineFrame::Data(b"shallow 3333333333333333333333333333333333333333\n".to_vec()),
10780            PktLineFrame::Data(b"deepen-since 1710000000\n".to_vec()),
10781            PktLineFrame::Data(b"deepen-not refs/tags/base\n".to_vec()),
10782            PktLineFrame::Data(b"filter blob:none\n".to_vec()),
10783            PktLineFrame::Flush,
10784        ];
10785        let request = parse_upload_pack_request(ObjectFormat::Sha1, &frames)
10786            .expect("test operation should succeed")
10787            .expect("test operation should succeed");
10788        assert_eq!(
10789            request,
10790            UploadPackRequest {
10791                wants: vec![want, second_want],
10792                capabilities: vec![
10793                    Capability {
10794                        name: "multi_ack".into(),
10795                        value: None,
10796                    },
10797                    Capability {
10798                        name: "thin-pack".into(),
10799                        value: None,
10800                    },
10801                    Capability {
10802                        name: "agent".into(),
10803                        value: Some("git/2.54.0".into()),
10804                    },
10805                ],
10806                shallow: vec![shallow],
10807                deepen: None,
10808                deepen_since: Some(1_710_000_000),
10809                deepen_not: vec!["refs/tags/base".into()],
10810                filter: Some("blob:none".into()),
10811            }
10812        );
10813        assert_eq!(
10814            encode_upload_pack_request(Some(&request)).expect("test operation should succeed"),
10815            frames
10816        );
10817        assert_eq!(
10818            parse_upload_pack_request(ObjectFormat::Sha1, &[PktLineFrame::Flush])
10819                .expect("test operation should succeed"),
10820            None
10821        );
10822        assert_eq!(
10823            encode_upload_pack_request(None).expect("test operation should succeed"),
10824            vec![PktLineFrame::Flush]
10825        );
10826    }
10827
10828    #[test]
10829    fn upload_pack_request_streams_round_trip() {
10830        let request = UploadPackRequest {
10831            wants: vec![
10832                ObjectId::from_hex(
10833                    ObjectFormat::Sha1,
10834                    "1111111111111111111111111111111111111111",
10835                )
10836                .expect("test operation should succeed"),
10837            ],
10838            capabilities: vec![Capability {
10839                name: "ofs-delta".into(),
10840                value: None,
10841            }],
10842            deepen: Some(10),
10843            ..UploadPackRequest::default()
10844        };
10845        let mut encoded = Vec::new();
10846        write_upload_pack_request(&mut encoded, Some(&request))
10847            .expect("test operation should succeed");
10848        encoded.extend_from_slice(b"tail");
10849
10850        let mut input = encoded.as_slice();
10851        assert_eq!(
10852            read_upload_pack_request(ObjectFormat::Sha1, &mut input)
10853                .expect("test operation should succeed"),
10854            Some(request)
10855        );
10856        assert_eq!(input, b"tail");
10857    }
10858
10859    #[test]
10860    fn upload_pack_request_rejects_malformed_requests() {
10861        assert!(
10862            parse_upload_pack_request(
10863                ObjectFormat::Sha1,
10864                &[PktLineFrame::Data(
10865                    b"want 1111111111111111111111111111111111111111\n".to_vec(),
10866                )],
10867            )
10868            .is_err()
10869        );
10870        assert!(
10871            parse_upload_pack_request(
10872                ObjectFormat::Sha1,
10873                &[
10874                    PktLineFrame::Data(
10875                        b"shallow 1111111111111111111111111111111111111111\n".to_vec(),
10876                    ),
10877                    PktLineFrame::Flush,
10878                ],
10879            )
10880            .is_err()
10881        );
10882        assert!(
10883            parse_upload_pack_request(
10884                ObjectFormat::Sha1,
10885                &[
10886                    PktLineFrame::Data(
10887                        b"want 1111111111111111111111111111111111111111 thin-pack\n".to_vec(),
10888                    ),
10889                    PktLineFrame::Data(
10890                        b"want 2222222222222222222222222222222222222222 ofs-delta\n".to_vec(),
10891                    ),
10892                    PktLineFrame::Flush,
10893                ],
10894            )
10895            .is_err()
10896        );
10897        assert!(parse_upload_pack_request(
10898            ObjectFormat::Sha1,
10899            &[
10900                PktLineFrame::Data(b"want 1111111111111111111111111111111111111111\n".to_vec(),),
10901                PktLineFrame::Data(b"deepen 1\n".to_vec()),
10902                PktLineFrame::Data(b"want 2222222222222222222222222222222222222222\n".to_vec()),
10903                PktLineFrame::Flush,
10904            ],
10905        )
10906        .is_err());
10907        assert!(parse_upload_pack_request(
10908            ObjectFormat::Sha1,
10909            &[
10910                PktLineFrame::Data(b"want 1111111111111111111111111111111111111111\n".to_vec(),),
10911                PktLineFrame::Data(b"filter blob:none\n".to_vec()),
10912                PktLineFrame::Data(b"filter tree:0\n".to_vec()),
10913                PktLineFrame::Flush,
10914            ],
10915        )
10916        .is_err());
10917        assert!(encode_upload_pack_request(Some(&UploadPackRequest::default())).is_err());
10918        assert!(
10919            encode_upload_pack_request(Some(&UploadPackRequest {
10920                wants: vec![
10921                    ObjectId::from_hex(
10922                        ObjectFormat::Sha1,
10923                        "1111111111111111111111111111111111111111",
10924                    )
10925                    .expect("test operation should succeed")
10926                ],
10927                deepen: Some(0),
10928                ..UploadPackRequest::default()
10929            }))
10930            .is_err()
10931        );
10932    }
10933
10934    #[test]
10935    fn upload_pack_shallow_update_parses_and_encodes_records() {
10936        let shallow = ObjectId::from_hex(
10937            ObjectFormat::Sha1,
10938            "1111111111111111111111111111111111111111",
10939        )
10940        .expect("test operation should succeed");
10941        let unshallow = ObjectId::from_hex(
10942            ObjectFormat::Sha1,
10943            "2222222222222222222222222222222222222222",
10944        )
10945        .expect("test operation should succeed");
10946        let frames = vec![
10947            PktLineFrame::Data(b"shallow 1111111111111111111111111111111111111111\n".to_vec()),
10948            PktLineFrame::Data(b"unshallow 2222222222222222222222222222222222222222\n".to_vec()),
10949            PktLineFrame::Flush,
10950        ];
10951        let entries = parse_upload_pack_shallow_update(ObjectFormat::Sha1, &frames)
10952            .expect("test operation should succeed");
10953        assert_eq!(
10954            entries,
10955            vec![
10956                ProtocolV2FetchShallowInfo::Shallow(shallow),
10957                ProtocolV2FetchShallowInfo::Unshallow(unshallow),
10958            ]
10959        );
10960        assert_eq!(
10961            encode_upload_pack_shallow_update(&entries).expect("test operation should succeed"),
10962            frames
10963        );
10964        assert_eq!(
10965            parse_upload_pack_shallow_update(ObjectFormat::Sha1, &[PktLineFrame::Flush])
10966                .expect("test operation should succeed"),
10967            Vec::<ProtocolV2FetchShallowInfo>::new()
10968        );
10969    }
10970
10971    #[test]
10972    fn upload_pack_shallow_update_streams_round_trip() {
10973        let entries = vec![ProtocolV2FetchShallowInfo::Shallow(
10974            ObjectId::from_hex(
10975                ObjectFormat::Sha1,
10976                "1111111111111111111111111111111111111111",
10977            )
10978            .expect("test operation should succeed"),
10979        )];
10980        let mut encoded = Vec::new();
10981        write_upload_pack_shallow_update(&mut encoded, &entries)
10982            .expect("test operation should succeed");
10983        encoded.extend_from_slice(b"tail");
10984
10985        let mut input = encoded.as_slice();
10986        assert_eq!(
10987            read_upload_pack_shallow_update(ObjectFormat::Sha1, &mut input)
10988                .expect("test operation should succeed"),
10989            entries
10990        );
10991        assert_eq!(input, b"tail");
10992    }
10993
10994    #[test]
10995    fn upload_pack_shallow_update_rejects_malformed_records() {
10996        assert!(
10997            parse_upload_pack_shallow_update(
10998                ObjectFormat::Sha1,
10999                &[PktLineFrame::Data(
11000                    b"shallow 1111111111111111111111111111111111111111\n".to_vec(),
11001                )],
11002            )
11003            .is_err()
11004        );
11005        assert!(
11006            parse_upload_pack_shallow_update(
11007                ObjectFormat::Sha1,
11008                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
11009            )
11010            .is_err()
11011        );
11012        assert!(
11013            parse_upload_pack_shallow_update(
11014                ObjectFormat::Sha1,
11015                &[
11016                    PktLineFrame::Data(
11017                        b"shallow 1111111111111111111111111111111111111111\n".to_vec(),
11018                    ),
11019                    PktLineFrame::Flush,
11020                    PktLineFrame::Data(
11021                        b"unshallow 2222222222222222222222222222222222222222\n".to_vec(),
11022                    ),
11023                ],
11024            )
11025            .is_err()
11026        );
11027        assert!(
11028            parse_upload_pack_shallow_update(
11029                ObjectFormat::Sha1,
11030                &[
11031                    PktLineFrame::Data(
11032                        b"unsupported 1111111111111111111111111111111111111111\n".to_vec(),
11033                    ),
11034                    PktLineFrame::Flush,
11035                ],
11036            )
11037            .is_err()
11038        );
11039    }
11040
11041    #[test]
11042    fn upload_pack_negotiation_request_parses_flush_and_done_rounds() {
11043        let have = ObjectId::from_hex(
11044            ObjectFormat::Sha1,
11045            "1111111111111111111111111111111111111111",
11046        )
11047        .expect("test operation should succeed");
11048        let second_have = ObjectId::from_hex(
11049            ObjectFormat::Sha1,
11050            "2222222222222222222222222222222222222222",
11051        )
11052        .expect("test operation should succeed");
11053        let flush_round = vec![
11054            PktLineFrame::Data(b"have 1111111111111111111111111111111111111111\n".to_vec()),
11055            PktLineFrame::Data(b"have 2222222222222222222222222222222222222222\n".to_vec()),
11056            PktLineFrame::Flush,
11057        ];
11058        let request = parse_upload_pack_negotiation_request(ObjectFormat::Sha1, &flush_round)
11059            .expect("test operation should succeed");
11060        assert_eq!(
11061            request,
11062            UploadPackNegotiationRequest {
11063                haves: vec![have, second_have],
11064                done: false,
11065            }
11066        );
11067        assert_eq!(
11068            encode_upload_pack_negotiation_request(&request)
11069                .expect("test operation should succeed"),
11070            flush_round
11071        );
11072
11073        let done_round = vec![
11074            PktLineFrame::Data(b"have 1111111111111111111111111111111111111111\n".to_vec()),
11075            PktLineFrame::Data(b"done\n".to_vec()),
11076        ];
11077        let request = parse_upload_pack_negotiation_request(ObjectFormat::Sha1, &done_round)
11078            .expect("test operation should succeed");
11079        assert_eq!(
11080            request,
11081            UploadPackNegotiationRequest {
11082                haves: vec![have],
11083                done: true,
11084            }
11085        );
11086        assert_eq!(
11087            encode_upload_pack_negotiation_request(&request)
11088                .expect("test operation should succeed"),
11089            done_round
11090        );
11091    }
11092
11093    #[test]
11094    fn upload_pack_negotiation_request_streams_round_trip() {
11095        let first = UploadPackNegotiationRequest {
11096            haves: vec![
11097                ObjectId::from_hex(
11098                    ObjectFormat::Sha1,
11099                    "1111111111111111111111111111111111111111",
11100                )
11101                .expect("test operation should succeed"),
11102            ],
11103            done: false,
11104        };
11105        let second = UploadPackNegotiationRequest {
11106            haves: Vec::new(),
11107            done: true,
11108        };
11109        let mut encoded = Vec::new();
11110        write_upload_pack_negotiation_request(&mut encoded, &first)
11111            .expect("test operation should succeed");
11112        write_upload_pack_negotiation_request(&mut encoded, &second)
11113            .expect("test operation should succeed");
11114        encoded.extend_from_slice(b"tail");
11115
11116        let mut input = encoded.as_slice();
11117        assert_eq!(
11118            read_upload_pack_negotiation_request(ObjectFormat::Sha1, &mut input)
11119                .expect("test operation should succeed"),
11120            first
11121        );
11122        assert_eq!(
11123            read_upload_pack_negotiation_request(ObjectFormat::Sha1, &mut input)
11124                .expect("test operation should succeed"),
11125            second
11126        );
11127        assert_eq!(input, b"tail");
11128    }
11129
11130    #[test]
11131    fn upload_pack_negotiation_request_rejects_malformed_rounds() {
11132        assert!(
11133            parse_upload_pack_negotiation_request(
11134                ObjectFormat::Sha1,
11135                &[PktLineFrame::Data(
11136                    b"have 1111111111111111111111111111111111111111\n".to_vec(),
11137                )],
11138            )
11139            .is_err()
11140        );
11141        assert!(
11142            parse_upload_pack_negotiation_request(
11143                ObjectFormat::Sha1,
11144                &[PktLineFrame::Data(
11145                    b"want 1111111111111111111111111111111111111111\n".to_vec(),
11146                )],
11147            )
11148            .is_err()
11149        );
11150        assert!(parse_upload_pack_negotiation_request(
11151            ObjectFormat::Sha1,
11152            &[
11153                PktLineFrame::Data(b"done\n".to_vec()),
11154                PktLineFrame::Data(b"have 1111111111111111111111111111111111111111\n".to_vec(),),
11155            ],
11156        )
11157        .is_err());
11158        assert!(
11159            parse_upload_pack_negotiation_request(
11160                ObjectFormat::Sha1,
11161                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
11162            )
11163            .is_err()
11164        );
11165    }
11166
11167    #[test]
11168    fn upload_pack_acknowledgments_parse_and_encode_statuses() {
11169        let oid = ObjectId::from_hex(
11170            ObjectFormat::Sha1,
11171            "1111111111111111111111111111111111111111",
11172        )
11173        .expect("test operation should succeed");
11174        assert_eq!(
11175            parse_upload_pack_acknowledgment(ObjectFormat::Sha1, b"NAK\n")
11176                .expect("test operation should succeed"),
11177            UploadPackAcknowledgment::Nak
11178        );
11179        for (payload, status) in [
11180            (
11181                b"ACK 1111111111111111111111111111111111111111\n".as_slice(),
11182                None,
11183            ),
11184            (
11185                b"ACK 1111111111111111111111111111111111111111 continue\n".as_slice(),
11186                Some(UploadPackAckStatus::Continue),
11187            ),
11188            (
11189                b"ACK 1111111111111111111111111111111111111111 common\n".as_slice(),
11190                Some(UploadPackAckStatus::Common),
11191            ),
11192            (
11193                b"ACK 1111111111111111111111111111111111111111 ready\n".as_slice(),
11194                Some(UploadPackAckStatus::Ready),
11195            ),
11196        ] {
11197            let acknowledgment = parse_upload_pack_acknowledgment(ObjectFormat::Sha1, payload)
11198                .expect("test operation should succeed");
11199            assert_eq!(
11200                acknowledgment,
11201                UploadPackAcknowledgment::Ack { oid, status }
11202            );
11203            assert_eq!(
11204                encode_upload_pack_acknowledgment(&acknowledgment)
11205                    .expect("test operation should succeed"),
11206                payload
11207            );
11208        }
11209    }
11210
11211    #[test]
11212    fn upload_pack_acknowledgments_stream_round_trip_and_reject_bad_lines() {
11213        let acknowledgment = UploadPackAcknowledgment::Ack {
11214            oid: ObjectId::from_hex(
11215                ObjectFormat::Sha1,
11216                "1111111111111111111111111111111111111111",
11217            )
11218            .expect("test operation should succeed"),
11219            status: Some(UploadPackAckStatus::Ready),
11220        };
11221        let mut encoded = Vec::new();
11222        write_upload_pack_acknowledgment(&mut encoded, &acknowledgment)
11223            .expect("test operation should succeed");
11224        encoded.extend_from_slice(b"tail");
11225
11226        let mut input = encoded.as_slice();
11227        assert_eq!(
11228            read_upload_pack_acknowledgment(ObjectFormat::Sha1, &mut input)
11229                .expect("test operation should succeed"),
11230            acknowledgment
11231        );
11232        assert_eq!(input, b"tail");
11233        assert!(parse_upload_pack_acknowledgment(ObjectFormat::Sha1, b"ACK not-an-oid\n").is_err());
11234        assert!(
11235            parse_upload_pack_acknowledgment(
11236                ObjectFormat::Sha1,
11237                b"ACK 1111111111111111111111111111111111111111 unknown\n",
11238            )
11239            .is_err()
11240        );
11241        assert!(
11242            parse_upload_pack_acknowledgment(
11243                ObjectFormat::Sha1,
11244                b"ACK 1111111111111111111111111111111111111111 ready extra\n",
11245            )
11246            .is_err()
11247        );
11248        assert!(
11249            parse_upload_pack_acknowledgment(ObjectFormat::Sha1, b"ERR remote died\n").is_err()
11250        );
11251        assert!(read_upload_pack_acknowledgment(ObjectFormat::Sha1, &mut &b"0000"[..]).is_err());
11252    }
11253
11254    #[test]
11255    fn upload_pack_packfile_response_parses_acknowledgments_and_sideband() {
11256        let oid = ObjectId::from_hex(
11257            ObjectFormat::Sha1,
11258            "1111111111111111111111111111111111111111",
11259        )
11260        .expect("test operation should succeed");
11261        let frames = vec![
11262            PktLineFrame::Data(b"ACK 1111111111111111111111111111111111111111 common\n".to_vec()),
11263            PktLineFrame::Data(b"NAK\n".to_vec()),
11264            PktLineFrame::Data(b"\x01PACK".to_vec()),
11265            PktLineFrame::Data(b"\x02counting objects\n".to_vec()),
11266            PktLineFrame::Data(b"\x01 bytes".to_vec()),
11267            PktLineFrame::Flush,
11268        ];
11269        let response = parse_upload_pack_packfile_response(ObjectFormat::Sha1, &frames)
11270            .expect("test operation should succeed");
11271        assert_eq!(
11272            response,
11273            UploadPackPackfileResponse {
11274                acknowledgments: vec![
11275                    UploadPackAcknowledgment::Ack {
11276                        oid,
11277                        status: Some(UploadPackAckStatus::Common),
11278                    },
11279                    UploadPackAcknowledgment::Nak,
11280                ],
11281                sideband: vec![
11282                    SideBandPacket {
11283                        channel: SideBandChannel::Data,
11284                        data: b"PACK".to_vec(),
11285                    },
11286                    SideBandPacket {
11287                        channel: SideBandChannel::Progress,
11288                        data: b"counting objects\n".to_vec(),
11289                    },
11290                    SideBandPacket {
11291                        channel: SideBandChannel::Data,
11292                        data: b" bytes".to_vec(),
11293                    },
11294                ],
11295            }
11296        );
11297        assert_eq!(
11298            demux_upload_pack_packfile_response(&response).expect("test operation should succeed"),
11299            SideBandDemux {
11300                data: b"PACK bytes".to_vec(),
11301                progress: vec![b"counting objects\n".to_vec()],
11302            }
11303        );
11304        assert_eq!(
11305            encode_upload_pack_packfile_response(&response).expect("test operation should succeed"),
11306            frames
11307        );
11308    }
11309
11310    #[test]
11311    fn upload_pack_packfile_response_streams_round_trip() {
11312        let response = UploadPackPackfileResponse {
11313            acknowledgments: vec![UploadPackAcknowledgment::Nak],
11314            sideband: vec![SideBandPacket {
11315                channel: SideBandChannel::Data,
11316                data: b"PACK".to_vec(),
11317            }],
11318        };
11319        let mut encoded = Vec::new();
11320        write_upload_pack_packfile_response(&mut encoded, &response)
11321            .expect("test operation should succeed");
11322        encoded.extend_from_slice(b"tail");
11323
11324        let mut input = encoded.as_slice();
11325        assert_eq!(
11326            read_upload_pack_packfile_response(ObjectFormat::Sha1, &mut input)
11327                .expect("test operation should succeed"),
11328            response
11329        );
11330        assert_eq!(input, b"tail");
11331    }
11332
11333    #[test]
11334    fn upload_pack_packfile_response_rejects_malformed_streams() {
11335        assert!(
11336            parse_upload_pack_packfile_response(
11337                ObjectFormat::Sha1,
11338                &[PktLineFrame::Data(b"NAK\n".to_vec())],
11339            )
11340            .is_err()
11341        );
11342        assert!(
11343            parse_upload_pack_packfile_response(
11344                ObjectFormat::Sha1,
11345                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
11346            )
11347            .is_err()
11348        );
11349        assert!(
11350            parse_upload_pack_packfile_response(
11351                ObjectFormat::Sha1,
11352                &[
11353                    PktLineFrame::Data(b"\x01PACK".to_vec()),
11354                    PktLineFrame::Data(
11355                        b"ACK 1111111111111111111111111111111111111111 common\n".to_vec()
11356                    ),
11357                    PktLineFrame::Flush,
11358                ],
11359            )
11360            .is_err()
11361        );
11362        assert!(
11363            parse_upload_pack_packfile_response(
11364                ObjectFormat::Sha1,
11365                &[
11366                    PktLineFrame::Data(b"NAK\n".to_vec()),
11367                    PktLineFrame::Flush,
11368                    PktLineFrame::Data(b"\x01PACK".to_vec()),
11369                ],
11370            )
11371            .is_err()
11372        );
11373        assert!(
11374            parse_upload_pack_packfile_response(
11375                ObjectFormat::Sha1,
11376                &[
11377                    PktLineFrame::Data(b"NAK\n".to_vec()),
11378                    PktLineFrame::Data(b"\x04bad".to_vec()),
11379                    PktLineFrame::Flush,
11380                ],
11381            )
11382            .is_err()
11383        );
11384    }
11385
11386    #[test]
11387    fn upload_pack_raw_packfile_response_parses_acknowledgments_and_raw_pack() {
11388        let oid = ObjectId::from_hex(
11389            ObjectFormat::Sha1,
11390            "1111111111111111111111111111111111111111",
11391        )
11392        .expect("test operation should succeed");
11393        let response = UploadPackRawPackfileResponse {
11394            acknowledgments: vec![
11395                UploadPackAcknowledgment::Ack {
11396                    oid,
11397                    status: Some(UploadPackAckStatus::Common),
11398                },
11399                UploadPackAcknowledgment::Nak,
11400            ],
11401            packfile: b"PACK\x00\x00\x00\x02raw-bytes".to_vec(),
11402        };
11403        let encoded = encode_upload_pack_raw_packfile_response(&response)
11404            .expect("test operation should succeed");
11405        assert_eq!(
11406            parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &encoded)
11407                .expect("test operation should succeed"),
11408            response
11409        );
11410    }
11411
11412    #[test]
11413    fn upload_pack_raw_packfile_response_streams_round_trip() {
11414        let response = UploadPackRawPackfileResponse {
11415            acknowledgments: vec![UploadPackAcknowledgment::Nak],
11416            packfile: b"PACK\x00\x00\x00\x02raw-bytes".to_vec(),
11417        };
11418        let mut encoded = Vec::new();
11419        write_upload_pack_raw_packfile_response(&mut encoded, &response)
11420            .expect("test operation should succeed");
11421        assert_eq!(
11422            encoded,
11423            encode_upload_pack_raw_packfile_response(&response)
11424                .expect("test operation should succeed")
11425        );
11426
11427        let mut input = encoded.as_slice();
11428        assert_eq!(
11429            read_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &mut input)
11430                .expect("test operation should succeed"),
11431            response
11432        );
11433        assert!(input.is_empty());
11434    }
11435
11436    #[test]
11437    fn upload_pack_raw_packfile_response_rejects_malformed_streams() {
11438        let ack = PktLineFrame::data(b"NAK\n".to_vec())
11439            .expect("test operation should succeed")
11440            .try_encode()
11441            .expect("test operation should succeed");
11442        let bad_ack = PktLineFrame::data(b"ACK not-an-oid\n".to_vec())
11443            .expect("test operation should succeed")
11444            .try_encode()
11445            .expect("test operation should succeed");
11446        let non_ack =
11447            PktLineFrame::data(b"have 1111111111111111111111111111111111111111\n".to_vec())
11448                .expect("test operation should succeed")
11449                .try_encode()
11450                .expect("test operation should succeed");
11451        let mut garbage_after_ack = ack.clone();
11452        garbage_after_ack.extend_from_slice(b"garbage");
11453
11454        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, b"").is_err());
11455        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &ack).is_err());
11456        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &bad_ack).is_err());
11457        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, b"0000PACK").is_err());
11458        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &non_ack).is_err());
11459        assert!(
11460            parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &garbage_after_ack)
11461                .is_err()
11462        );
11463        assert!(
11464            encode_upload_pack_raw_packfile_response(&UploadPackRawPackfileResponse {
11465                acknowledgments: vec![UploadPackAcknowledgment::Nak],
11466                packfile: Vec::new(),
11467            })
11468            .is_err()
11469        );
11470        assert!(
11471            encode_upload_pack_raw_packfile_response(&UploadPackRawPackfileResponse {
11472                acknowledgments: Vec::new(),
11473                packfile: b"not-a-pack".to_vec(),
11474            })
11475            .is_err()
11476        );
11477    }
11478
11479    #[test]
11480    fn upload_pack_request_encodes_deepen_request() {
11481        // A `--depth 1` clone over smart-HTTP v1: the `want` line carries the
11482        // capabilities, the client's existing shallow boundary is replayed as a
11483        // `shallow` line, and `deepen 1` requests the truncation. Built as raw
11484        // pkt-line bytes so the 4-hex length prefixes are exercised.
11485        let want = ObjectId::from_hex(
11486            ObjectFormat::Sha1,
11487            "1111111111111111111111111111111111111111",
11488        )
11489        .expect("test operation should succeed");
11490        let boundary = ObjectId::from_hex(
11491            ObjectFormat::Sha1,
11492            "2222222222222222222222222222222222222222",
11493        )
11494        .expect("test operation should succeed");
11495        let request = UploadPackRequest {
11496            wants: vec![want],
11497            capabilities: vec![Capability {
11498                name: "shallow".into(),
11499                value: None,
11500            }],
11501            shallow: vec![boundary],
11502            deepen: Some(1),
11503            ..UploadPackRequest::default()
11504        };
11505        let mut encoded = Vec::new();
11506        write_upload_pack_request(&mut encoded, Some(&request))
11507            .expect("test operation should succeed");
11508        let mut expected = Vec::new();
11509        expected.extend_from_slice(b"003awant 1111111111111111111111111111111111111111 shallow\n");
11510        expected.extend_from_slice(b"0035shallow 2222222222222222222222222222222222222222\n");
11511        expected.extend_from_slice(b"000ddeepen 1\n");
11512        expected.extend_from_slice(b"0000");
11513        assert_eq!(encoded, expected);
11514    }
11515
11516    #[test]
11517    fn upload_pack_shallow_info_response_parses_shallow_unshallow_and_pack() {
11518        // The smart-HTTP v1 deepen response: a shallow-info section (one
11519        // `shallow` and one `unshallow` line) terminated by a flush, then the
11520        // NAK and the raw packfile. Hand-built pkt-lines (mind the lengths).
11521        let shallow = ObjectId::from_hex(
11522            ObjectFormat::Sha1,
11523            "1111111111111111111111111111111111111111",
11524        )
11525        .expect("test operation should succeed");
11526        let unshallow = ObjectId::from_hex(
11527            ObjectFormat::Sha1,
11528            "2222222222222222222222222222222222222222",
11529        )
11530        .expect("test operation should succeed");
11531        let mut input = Vec::new();
11532        input.extend_from_slice(b"0035shallow 1111111111111111111111111111111111111111\n");
11533        input.extend_from_slice(b"0037unshallow 2222222222222222222222222222222222222222\n");
11534        input.extend_from_slice(b"0000"); // shallow-info terminator
11535        input.extend_from_slice(b"0008NAK\n");
11536        input.extend_from_slice(b"PACK\x00\x00\x00\x02raw-bytes");
11537
11538        let (entries, response) =
11539            parse_upload_pack_shallow_info_and_raw_packfile_response(ObjectFormat::Sha1, &input)
11540                .expect("test operation should succeed");
11541        assert_eq!(
11542            entries,
11543            vec![
11544                ProtocolV2FetchShallowInfo::Shallow(shallow),
11545                ProtocolV2FetchShallowInfo::Unshallow(unshallow),
11546            ]
11547        );
11548        assert_eq!(
11549            response,
11550            UploadPackRawPackfileResponse {
11551                acknowledgments: vec![UploadPackAcknowledgment::Nak],
11552                packfile: b"PACK\x00\x00\x00\x02raw-bytes".to_vec(),
11553            }
11554        );
11555
11556        // The reader entry point yields the same result over a stream.
11557        let mut stream = input.as_slice();
11558        let (read_entries, read_response) =
11559            read_upload_pack_shallow_info_and_raw_packfile_response(
11560                ObjectFormat::Sha1,
11561                &mut stream,
11562            )
11563            .expect("test operation should succeed");
11564        assert_eq!(read_entries, entries);
11565        assert_eq!(read_response, response);
11566    }
11567
11568    #[test]
11569    fn upload_pack_shallow_info_response_handles_empty_shallow_section() {
11570        // A deepen request that creates no boundary change still gets an empty
11571        // shallow-info section (a bare flush) before the NAK + pack.
11572        let mut input = Vec::new();
11573        input.extend_from_slice(b"0000"); // empty shallow-info
11574        input.extend_from_slice(b"0008NAK\n");
11575        input.extend_from_slice(b"PACK\x00\x00\x00\x02raw-bytes");
11576
11577        let (entries, response) =
11578            parse_upload_pack_shallow_info_and_raw_packfile_response(ObjectFormat::Sha1, &input)
11579                .expect("test operation should succeed");
11580        assert!(entries.is_empty());
11581        assert_eq!(
11582            response.acknowledgments,
11583            vec![UploadPackAcknowledgment::Nak]
11584        );
11585        assert!(response.packfile.starts_with(b"PACK"));
11586    }
11587
11588    #[test]
11589    fn upload_pack_shallow_info_response_rejects_malformed_sections() {
11590        // Truncated section (no terminating flush before EOF).
11591        let truncated = b"0035shallow 1111111111111111111111111111111111111111\n".to_vec();
11592        assert!(
11593            parse_upload_pack_shallow_info_and_raw_packfile_response(
11594                ObjectFormat::Sha1,
11595                &truncated
11596            )
11597            .is_err()
11598        );
11599        // A non-flush control packet inside the shallow-info section.
11600        let mut delimiter_section = Vec::new();
11601        delimiter_section.extend_from_slice(b"0001"); // delimiter, not a flush
11602        assert!(
11603            parse_upload_pack_shallow_info_section(ObjectFormat::Sha1, &delimiter_section).is_err()
11604        );
11605        // A non-shallow data line inside the section.
11606        let mut bad_line = Vec::new();
11607        bad_line.extend_from_slice(b"0008NAK\n");
11608        assert!(parse_upload_pack_shallow_info_section(ObjectFormat::Sha1, &bad_line).is_err());
11609        // Valid shallow-info but a missing packfile afterwards.
11610        let mut no_pack = Vec::new();
11611        no_pack.extend_from_slice(b"0000"); // empty shallow-info
11612        no_pack.extend_from_slice(b"0008NAK\n");
11613        assert!(
11614            parse_upload_pack_shallow_info_and_raw_packfile_response(ObjectFormat::Sha1, &no_pack)
11615                .is_err()
11616        );
11617    }
11618
11619    #[test]
11620    fn receive_pack_request_parses_and_encodes_commands() {
11621        let old_id = ObjectId::from_hex(
11622            ObjectFormat::Sha1,
11623            "1111111111111111111111111111111111111111",
11624        )
11625        .expect("test operation should succeed");
11626        let new_id = ObjectId::from_hex(
11627            ObjectFormat::Sha1,
11628            "2222222222222222222222222222222222222222",
11629        )
11630        .expect("test operation should succeed");
11631        let delete_old_id = ObjectId::from_hex(
11632            ObjectFormat::Sha1,
11633            "3333333333333333333333333333333333333333",
11634        )
11635        .expect("test operation should succeed");
11636        let zero = ObjectId::from_hex(
11637            ObjectFormat::Sha1,
11638            "0000000000000000000000000000000000000000",
11639        )
11640        .expect("test operation should succeed");
11641        let shallow = ObjectId::from_hex(
11642            ObjectFormat::Sha1,
11643            "4444444444444444444444444444444444444444",
11644        )
11645        .expect("test operation should succeed");
11646        let frames = vec![
11647            PktLineFrame::Data(b"shallow 4444444444444444444444444444444444444444\n".to_vec()),
11648            PktLineFrame::Data(
11649                b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\0report-status side-band-64k agent=git/2.54.0\n"
11650                    .to_vec(),
11651            ),
11652            PktLineFrame::Data(
11653                b"3333333333333333333333333333333333333333 0000000000000000000000000000000000000000 refs/heads/old\n"
11654                    .to_vec(),
11655            ),
11656            PktLineFrame::Flush,
11657        ];
11658        let request = parse_receive_pack_request(ObjectFormat::Sha1, &frames)
11659            .expect("test operation should succeed");
11660        assert_eq!(
11661            request,
11662            ReceivePackRequest {
11663                shallow: vec![shallow],
11664                commands: vec![
11665                    ReceivePackCommand {
11666                        old_id,
11667                        new_id,
11668                        name: "refs/heads/main".into(),
11669                    },
11670                    ReceivePackCommand {
11671                        old_id: delete_old_id,
11672                        new_id: zero,
11673                        name: "refs/heads/old".into(),
11674                    },
11675                ],
11676                capabilities: vec![
11677                    Capability {
11678                        name: "report-status".into(),
11679                        value: None,
11680                    },
11681                    Capability {
11682                        name: "side-band-64k".into(),
11683                        value: None,
11684                    },
11685                    Capability {
11686                        name: "agent".into(),
11687                        value: Some("git/2.54.0".into()),
11688                    },
11689                ],
11690            }
11691        );
11692        assert_eq!(
11693            encode_receive_pack_request(&request).expect("test operation should succeed"),
11694            frames
11695        );
11696        assert_eq!(
11697            parse_receive_pack_request(ObjectFormat::Sha1, &[PktLineFrame::Flush])
11698                .expect("test operation should succeed"),
11699            ReceivePackRequest::default()
11700        );
11701    }
11702
11703    #[test]
11704    fn receive_pack_request_streams_round_trip() {
11705        let request = ReceivePackRequest {
11706            commands: vec![ReceivePackCommand {
11707                old_id: ObjectId::from_hex(
11708                    ObjectFormat::Sha1,
11709                    "0000000000000000000000000000000000000000",
11710                )
11711                .expect("test operation should succeed"),
11712                new_id: ObjectId::from_hex(
11713                    ObjectFormat::Sha1,
11714                    "1111111111111111111111111111111111111111",
11715                )
11716                .expect("test operation should succeed"),
11717                name: "refs/heads/main".into(),
11718            }],
11719            capabilities: vec![Capability {
11720                name: "report-status".into(),
11721                value: None,
11722            }],
11723            ..ReceivePackRequest::default()
11724        };
11725        let mut encoded = Vec::new();
11726        write_receive_pack_request(&mut encoded, &request).expect("test operation should succeed");
11727        encoded.extend_from_slice(b"PACK");
11728
11729        let mut input = encoded.as_slice();
11730        assert_eq!(
11731            read_receive_pack_request(ObjectFormat::Sha1, &mut input)
11732                .expect("test operation should succeed"),
11733            request
11734        );
11735        assert_eq!(input, b"PACK");
11736    }
11737
11738    #[test]
11739    fn receive_pack_request_rejects_malformed_commands() {
11740        assert!(
11741            parse_receive_pack_request(
11742                ObjectFormat::Sha1,
11743                &[PktLineFrame::Data(
11744                    b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\n"
11745                        .to_vec(),
11746                )],
11747            )
11748            .is_err()
11749        );
11750        assert!(
11751            parse_receive_pack_request(
11752                ObjectFormat::Sha1,
11753                &[
11754                    PktLineFrame::Data(
11755                        b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\n"
11756                            .to_vec(),
11757                    ),
11758                    PktLineFrame::Data(
11759                        b"shallow 3333333333333333333333333333333333333333\n".to_vec(),
11760                    ),
11761                    PktLineFrame::Flush,
11762                ],
11763            )
11764            .is_err()
11765        );
11766        assert!(
11767            parse_receive_pack_request(
11768                ObjectFormat::Sha1,
11769                &[
11770                    PktLineFrame::Data(
11771                        b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\0report-status\n"
11772                            .to_vec(),
11773                    ),
11774                    PktLineFrame::Data(
11775                        b"3333333333333333333333333333333333333333 4444444444444444444444444444444444444444 refs/heads/next\0side-band-64k\n"
11776                            .to_vec(),
11777                    ),
11778                    PktLineFrame::Flush,
11779                ],
11780            )
11781            .is_err()
11782        );
11783        assert!(
11784            parse_receive_pack_request(
11785                ObjectFormat::Sha1,
11786                &[
11787                    PktLineFrame::Data(
11788                        b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec(),
11789                    ),
11790                    PktLineFrame::Flush,
11791                ],
11792            )
11793            .is_err()
11794        );
11795        assert!(
11796            encode_receive_pack_request(&ReceivePackRequest {
11797                shallow: vec![
11798                    ObjectId::from_hex(
11799                        ObjectFormat::Sha1,
11800                        "1111111111111111111111111111111111111111",
11801                    )
11802                    .expect("test operation should succeed")
11803                ],
11804                ..ReceivePackRequest::default()
11805            })
11806            .is_err()
11807        );
11808        assert!(
11809            encode_receive_pack_request(&ReceivePackRequest {
11810                commands: vec![ReceivePackCommand {
11811                    old_id: ObjectId::from_hex(
11812                        ObjectFormat::Sha1,
11813                        "1111111111111111111111111111111111111111",
11814                    )
11815                    .expect("test operation should succeed"),
11816                    new_id: ObjectId::from_hex(
11817                        ObjectFormat::Sha1,
11818                        "2222222222222222222222222222222222222222",
11819                    )
11820                    .expect("test operation should succeed"),
11821                    name: "bad ref".into(),
11822                }],
11823                ..ReceivePackRequest::default()
11824            })
11825            .is_err()
11826        );
11827    }
11828
11829    #[test]
11830    fn receive_pack_features_parse_encode_and_validate_push_request() {
11831        let capabilities = vec![
11832            Capability {
11833                name: "report-status".into(),
11834                value: None,
11835            },
11836            Capability {
11837                name: "report-status-v2".into(),
11838                value: None,
11839            },
11840            Capability {
11841                name: "delete-refs".into(),
11842                value: None,
11843            },
11844            Capability {
11845                name: "ofs-delta".into(),
11846                value: None,
11847            },
11848            Capability {
11849                name: "atomic".into(),
11850                value: None,
11851            },
11852            Capability {
11853                name: "push-options".into(),
11854                value: None,
11855            },
11856            Capability {
11857                name: "side-band-64k".into(),
11858                value: None,
11859            },
11860            Capability {
11861                name: "quiet".into(),
11862                value: None,
11863            },
11864            Capability {
11865                name: "no-thin".into(),
11866                value: None,
11867            },
11868            Capability {
11869                name: "agent".into(),
11870                value: Some("git/2.54.0".into()),
11871            },
11872            Capability {
11873                name: "object-format".into(),
11874                value: Some("sha256".into()),
11875            },
11876            Capability {
11877                name: "custom".into(),
11878                value: Some("value".into()),
11879            },
11880        ];
11881        let features =
11882            parse_receive_pack_features(&capabilities).expect("test operation should succeed");
11883        assert_eq!(
11884            features,
11885            ReceivePackFeatures {
11886                report_status: true,
11887                report_status_v2: true,
11888                delete_refs: true,
11889                ofs_delta: true,
11890                atomic: true,
11891                push_options: true,
11892                side_band_64k: true,
11893                quiet: true,
11894                no_thin: true,
11895                agent: Some("git/2.54.0".into()),
11896                object_format: Some(ObjectFormat::Sha256),
11897                unknown: vec![Capability {
11898                    name: "custom".into(),
11899                    value: Some("value".into()),
11900                }],
11901            }
11902        );
11903        assert_eq!(
11904            encode_receive_pack_features(&features).expect("test operation should succeed"),
11905            capabilities
11906        );
11907
11908        let request = ReceivePackPushRequest {
11909            commands: ReceivePackRequest {
11910                commands: vec![ReceivePackCommand {
11911                    old_id: ObjectId::from_hex(
11912                        ObjectFormat::Sha1,
11913                        "1111111111111111111111111111111111111111",
11914                    )
11915                    .expect("test operation should succeed"),
11916                    new_id: ObjectId::from_hex(
11917                        ObjectFormat::Sha1,
11918                        "2222222222222222222222222222222222222222",
11919                    )
11920                    .expect("test operation should succeed"),
11921                    name: "refs/heads/main".into(),
11922                }],
11923                capabilities: vec![
11924                    Capability {
11925                        name: "report-status".into(),
11926                        value: None,
11927                    },
11928                    Capability {
11929                        name: "ofs-delta".into(),
11930                        value: None,
11931                    },
11932                    Capability {
11933                        name: "push-options".into(),
11934                        value: None,
11935                    },
11936                    Capability {
11937                        name: "side-band-64k".into(),
11938                        value: None,
11939                    },
11940                    Capability {
11941                        name: "agent".into(),
11942                        value: Some("sley".into()),
11943                    },
11944                ],
11945                ..ReceivePackRequest::default()
11946            },
11947            push_options: Some(vec!["ci.skip".into()]),
11948            packfile: b"PACKpayload".to_vec(),
11949        };
11950        validate_receive_pack_push_request_features(&features, &request)
11951            .expect("test operation should succeed");
11952    }
11953
11954    #[test]
11955    fn receive_pack_features_reject_invalid_push_requests() {
11956        let old_id = ObjectId::from_hex(
11957            ObjectFormat::Sha1,
11958            "1111111111111111111111111111111111111111",
11959        )
11960        .expect("test operation should succeed");
11961        let new_id = ObjectId::from_hex(
11962            ObjectFormat::Sha1,
11963            "2222222222222222222222222222222222222222",
11964        )
11965        .expect("test operation should succeed");
11966        let zero = ObjectId::from_hex(
11967            ObjectFormat::Sha1,
11968            "0000000000000000000000000000000000000000",
11969        )
11970        .expect("test operation should succeed");
11971        let features = ReceivePackFeatures {
11972            report_status: true,
11973            push_options: true,
11974            ..ReceivePackFeatures::default()
11975        };
11976        let update = ReceivePackCommand {
11977            old_id: old_id.clone(),
11978            new_id: new_id.clone(),
11979            name: "refs/heads/main".into(),
11980        };
11981
11982        assert!(
11983            validate_receive_pack_push_request_features(
11984                &features,
11985                &ReceivePackPushRequest {
11986                    commands: ReceivePackRequest {
11987                        commands: vec![update.clone()],
11988                        capabilities: vec![Capability {
11989                            name: "push-options".into(),
11990                            value: None,
11991                        }],
11992                        ..ReceivePackRequest::default()
11993                    },
11994                    push_options: None,
11995                    packfile: b"PACKpayload".to_vec(),
11996                },
11997            )
11998            .is_err()
11999        );
12000        assert!(
12001            validate_receive_pack_push_request_features(
12002                &features,
12003                &ReceivePackPushRequest {
12004                    commands: ReceivePackRequest {
12005                        commands: vec![update.clone()],
12006                        ..ReceivePackRequest::default()
12007                    },
12008                    push_options: Some(Vec::new()),
12009                    packfile: b"PACKpayload".to_vec(),
12010                },
12011            )
12012            .is_err()
12013        );
12014        assert!(
12015            validate_receive_pack_push_request_features(
12016                &features,
12017                &ReceivePackPushRequest {
12018                    commands: ReceivePackRequest {
12019                        commands: vec![ReceivePackCommand {
12020                            old_id: old_id.clone(),
12021                            new_id: zero.clone(),
12022                            name: "refs/heads/main".into(),
12023                        }],
12024                        ..ReceivePackRequest::default()
12025                    },
12026                    push_options: None,
12027                    packfile: Vec::new(),
12028                },
12029            )
12030            .is_err()
12031        );
12032        validate_receive_pack_push_request_features(
12033            &features,
12034            &ReceivePackPushRequest {
12035                commands: ReceivePackRequest {
12036                    commands: vec![update.clone()],
12037                    ..ReceivePackRequest::default()
12038                },
12039                push_options: None,
12040                packfile: Vec::new(),
12041            },
12042        )
12043        .expect("updates to already-present objects may omit a packfile");
12044        assert!(
12045            validate_receive_pack_push_request_features(
12046                &ReceivePackFeatures {
12047                    delete_refs: true,
12048                    ..ReceivePackFeatures::default()
12049                },
12050                &ReceivePackPushRequest {
12051                    commands: ReceivePackRequest {
12052                        commands: vec![ReceivePackCommand {
12053                            old_id,
12054                            new_id: zero,
12055                            name: "refs/heads/main".into(),
12056                        }],
12057                        ..ReceivePackRequest::default()
12058                    },
12059                    push_options: None,
12060                    packfile: b"PACKpayload".to_vec(),
12061                },
12062            )
12063            .is_err()
12064        );
12065        assert!(
12066            validate_receive_pack_push_request_features(
12067                &features,
12068                &ReceivePackPushRequest {
12069                    commands: ReceivePackRequest {
12070                        commands: vec![update],
12071                        capabilities: vec![Capability {
12072                            name: "atomic".into(),
12073                            value: None,
12074                        }],
12075                        ..ReceivePackRequest::default()
12076                    },
12077                    push_options: None,
12078                    packfile: b"PACKpayload".to_vec(),
12079                },
12080            )
12081            .is_err()
12082        );
12083
12084        assert!(
12085            parse_receive_pack_features(&[
12086                Capability {
12087                    name: "push-options".into(),
12088                    value: None,
12089                },
12090                Capability {
12091                    name: "push-options".into(),
12092                    value: None,
12093                },
12094            ])
12095            .is_err()
12096        );
12097        assert!(
12098            encode_receive_pack_features(&ReceivePackFeatures {
12099                unknown: vec![Capability {
12100                    name: "atomic".into(),
12101                    value: None,
12102                }],
12103                ..ReceivePackFeatures::default()
12104            })
12105            .is_err()
12106        );
12107    }
12108
12109    #[test]
12110    fn receive_pack_apply_helper_installs_pack_verifies_objects_and_reports_ok() {
12111        let old_id = ObjectId::from_hex(
12112            ObjectFormat::Sha1,
12113            "1111111111111111111111111111111111111111",
12114        )
12115        .expect("test operation should succeed");
12116        let new_id = ObjectId::from_hex(
12117            ObjectFormat::Sha1,
12118            "2222222222222222222222222222222222222222",
12119        )
12120        .expect("test operation should succeed");
12121        let request = ReceivePackPushRequest {
12122            commands: ReceivePackRequest {
12123                commands: vec![ReceivePackCommand {
12124                    old_id: old_id.clone(),
12125                    new_id: new_id.clone(),
12126                    name: "refs/heads/main".into(),
12127                }],
12128                ..ReceivePackRequest::default()
12129            },
12130            packfile: b"PACKpayload".to_vec(),
12131            ..ReceivePackPushRequest::default()
12132        };
12133        let installed = std::cell::Cell::new(false);
12134        let applied = std::cell::RefCell::new(Vec::new());
12135
12136        let report = apply_receive_pack_push_request(
12137            &ReceivePackFeatures::default(),
12138            &request,
12139            |_| unreachable!("update stale-old checks belong to the ref transaction callback"),
12140            |packfile| {
12141                assert_eq!(packfile, b"PACKpayload");
12142                installed.set(true);
12143                Ok(())
12144            },
12145            |oid| Ok(oid == &new_id),
12146            |commands| {
12147                applied.borrow_mut().extend_from_slice(commands);
12148                Ok(())
12149            },
12150            |_| unreachable!("no delete command should be applied"),
12151        )
12152        .expect("test operation should succeed");
12153
12154        assert!(installed.get());
12155        assert_eq!(applied.into_inner(), request.commands.commands);
12156        assert_eq!(report.unpack, ReceivePackUnpackStatus::Ok);
12157        assert_eq!(
12158            report.commands,
12159            vec![ReceivePackCommandStatus::Ok {
12160                name: "refs/heads/main".into(),
12161            }]
12162        );
12163    }
12164
12165    #[test]
12166    fn receive_pack_apply_helper_allows_update_without_pack_when_object_exists() {
12167        let old_id = ObjectId::from_hex(
12168            ObjectFormat::Sha1,
12169            "1111111111111111111111111111111111111111",
12170        )
12171        .expect("test operation should succeed");
12172        let new_id = ObjectId::from_hex(
12173            ObjectFormat::Sha1,
12174            "2222222222222222222222222222222222222222",
12175        )
12176        .expect("test operation should succeed");
12177        let request = ReceivePackPushRequest {
12178            commands: ReceivePackRequest {
12179                commands: vec![ReceivePackCommand {
12180                    old_id: old_id.clone(),
12181                    new_id: new_id.clone(),
12182                    name: "refs/heads/main".into(),
12183                }],
12184                ..ReceivePackRequest::default()
12185            },
12186            ..ReceivePackPushRequest::default()
12187        };
12188        let installed = std::cell::Cell::new(false);
12189        let applied = std::cell::RefCell::new(Vec::new());
12190
12191        let report = apply_receive_pack_push_request(
12192            &ReceivePackFeatures::default(),
12193            &request,
12194            |_| unreachable!("update stale-old checks belong to the ref transaction callback"),
12195            |_| {
12196                installed.set(true);
12197                Ok(())
12198            },
12199            |oid| Ok(oid == &new_id),
12200            |commands| {
12201                applied.borrow_mut().extend_from_slice(commands);
12202                Ok(())
12203            },
12204            |_| unreachable!("no delete command should be applied"),
12205        )
12206        .expect("test operation should succeed");
12207
12208        assert!(!installed.get());
12209        assert_eq!(applied.into_inner(), request.commands.commands);
12210        assert_eq!(report.unpack, ReceivePackUnpackStatus::Ok);
12211    }
12212
12213    #[test]
12214    fn receive_pack_apply_helper_preserves_delete_only_and_stale_delete_rules() {
12215        let old_id = ObjectId::from_hex(
12216            ObjectFormat::Sha1,
12217            "1111111111111111111111111111111111111111",
12218        )
12219        .expect("test operation should succeed");
12220        let other_id = ObjectId::from_hex(
12221            ObjectFormat::Sha1,
12222            "2222222222222222222222222222222222222222",
12223        )
12224        .expect("test operation should succeed");
12225        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
12226        let request = ReceivePackPushRequest {
12227            commands: ReceivePackRequest {
12228                commands: vec![ReceivePackCommand {
12229                    old_id: old_id.clone(),
12230                    new_id: zero,
12231                    name: "refs/heads/main".into(),
12232                }],
12233                ..ReceivePackRequest::default()
12234            },
12235            ..ReceivePackPushRequest::default()
12236        };
12237        let features = ReceivePackFeatures {
12238            delete_refs: true,
12239            ..ReceivePackFeatures::default()
12240        };
12241        let installed = std::cell::Cell::new(false);
12242        let deleted = std::cell::RefCell::new(Vec::new());
12243
12244        let report = apply_receive_pack_push_request(
12245            &features,
12246            &request,
12247            |_| Ok(Some(old_id.clone())),
12248            |_| {
12249                installed.set(true);
12250                Ok(())
12251            },
12252            |_| Ok(false),
12253            |_| unreachable!("delete-only request should not apply updates"),
12254            |command| {
12255                deleted.borrow_mut().push(command.name.clone());
12256                Ok(())
12257            },
12258        )
12259        .expect("test operation should succeed");
12260
12261        assert!(!installed.get());
12262        assert_eq!(deleted.into_inner(), vec!["refs/heads/main"]);
12263        assert_eq!(report.unpack, ReceivePackUnpackStatus::Ok);
12264        assert!(
12265            apply_receive_pack_push_request(
12266                &features,
12267                &request,
12268                |_| Ok(Some(other_id.clone())),
12269                |_| Ok(()),
12270                |_| Ok(false),
12271                |_| Ok(()),
12272                |_| Ok(()),
12273            )
12274            .is_err()
12275        );
12276    }
12277
12278    #[test]
12279    fn receive_pack_push_request_parses_commands_options_and_packfile() {
12280        let command = ReceivePackCommand {
12281            old_id: ObjectId::from_hex(
12282                ObjectFormat::Sha1,
12283                "1111111111111111111111111111111111111111",
12284            )
12285            .expect("test operation should succeed"),
12286            new_id: ObjectId::from_hex(
12287                ObjectFormat::Sha1,
12288                "2222222222222222222222222222222222222222",
12289            )
12290            .expect("test operation should succeed"),
12291            name: "refs/heads/main".into(),
12292        };
12293        let expected = ReceivePackPushRequest {
12294            commands: ReceivePackRequest {
12295                commands: vec![command],
12296                capabilities: vec![
12297                    Capability {
12298                        name: "report-status".into(),
12299                        value: None,
12300                    },
12301                    Capability {
12302                        name: "push-options".into(),
12303                        value: None,
12304                    },
12305                ],
12306                ..ReceivePackRequest::default()
12307            },
12308            push_options: Some(vec!["ci.skip".into(), "deploy=staging".into()]),
12309            packfile: b"PACK\x00\x00\x00\x02payload".to_vec(),
12310        };
12311        let encoded =
12312            encode_receive_pack_push_request(&expected).expect("test operation should succeed");
12313
12314        assert_eq!(
12315            parse_receive_pack_push_request(ObjectFormat::Sha1, &encoded, true)
12316                .expect("test operation should succeed"),
12317            expected
12318        );
12319    }
12320
12321    #[test]
12322    fn receive_pack_push_request_preserves_packfile_without_push_options() {
12323        let request = ReceivePackPushRequest {
12324            commands: ReceivePackRequest {
12325                commands: vec![ReceivePackCommand {
12326                    old_id: ObjectId::from_hex(
12327                        ObjectFormat::Sha1,
12328                        "1111111111111111111111111111111111111111",
12329                    )
12330                    .expect("test operation should succeed"),
12331                    new_id: ObjectId::from_hex(
12332                        ObjectFormat::Sha1,
12333                        "2222222222222222222222222222222222222222",
12334                    )
12335                    .expect("test operation should succeed"),
12336                    name: "refs/heads/main".into(),
12337                }],
12338                ..ReceivePackRequest::default()
12339            },
12340            push_options: None,
12341            packfile: b"0000PACK-like bytes stay raw".to_vec(),
12342        };
12343        let encoded =
12344            encode_receive_pack_push_request(&request).expect("test operation should succeed");
12345
12346        assert_eq!(
12347            parse_receive_pack_push_request(ObjectFormat::Sha1, &encoded, false)
12348                .expect("test operation should succeed"),
12349            request
12350        );
12351    }
12352
12353    #[test]
12354    fn receive_pack_push_request_streams_round_trip() {
12355        let request = ReceivePackPushRequest {
12356            commands: ReceivePackRequest {
12357                commands: vec![ReceivePackCommand {
12358                    old_id: ObjectId::from_hex(
12359                        ObjectFormat::Sha1,
12360                        "1111111111111111111111111111111111111111",
12361                    )
12362                    .expect("test operation should succeed"),
12363                    new_id: ObjectId::from_hex(
12364                        ObjectFormat::Sha1,
12365                        "2222222222222222222222222222222222222222",
12366                    )
12367                    .expect("test operation should succeed"),
12368                    name: "refs/heads/main".into(),
12369                }],
12370                capabilities: vec![Capability {
12371                    name: "push-options".into(),
12372                    value: None,
12373                }],
12374                ..ReceivePackRequest::default()
12375            },
12376            push_options: Some(Vec::new()),
12377            packfile: b"PACKpayload".to_vec(),
12378        };
12379        let mut encoded = Vec::new();
12380        write_receive_pack_push_request(&mut encoded, &request)
12381            .expect("test operation should succeed");
12382
12383        assert_eq!(
12384            read_receive_pack_push_request(ObjectFormat::Sha1, &mut encoded.as_slice(), true)
12385                .expect("test operation should succeed"),
12386            request
12387        );
12388    }
12389
12390    #[test]
12391    fn receive_pack_push_request_rejects_malformed_sections() {
12392        assert!(
12393            parse_receive_pack_push_request(
12394                ObjectFormat::Sha1,
12395                b"0014not-a-command\n0000PACK",
12396                false,
12397            )
12398            .is_err()
12399        );
12400
12401        let request = ReceivePackPushRequest {
12402            commands: ReceivePackRequest {
12403                commands: vec![ReceivePackCommand {
12404                    old_id: ObjectId::from_hex(
12405                        ObjectFormat::Sha1,
12406                        "1111111111111111111111111111111111111111",
12407                    )
12408                    .expect("test operation should succeed"),
12409                    new_id: ObjectId::from_hex(
12410                        ObjectFormat::Sha1,
12411                        "2222222222222222222222222222222222222222",
12412                    )
12413                    .expect("test operation should succeed"),
12414                    name: "refs/heads/main".into(),
12415                }],
12416                ..ReceivePackRequest::default()
12417            },
12418            push_options: None,
12419            packfile: b"PACKpayload".to_vec(),
12420        };
12421        let encoded =
12422            encode_receive_pack_push_request(&request).expect("test operation should succeed");
12423        assert!(parse_receive_pack_push_request(ObjectFormat::Sha1, &encoded, true).is_err());
12424
12425        assert!(
12426            encode_receive_pack_push_request(&ReceivePackPushRequest {
12427                commands: ReceivePackRequest {
12428                    shallow: vec![
12429                        ObjectId::from_hex(
12430                            ObjectFormat::Sha1,
12431                            "1111111111111111111111111111111111111111",
12432                        )
12433                        .expect("test operation should succeed")
12434                    ],
12435                    ..ReceivePackRequest::default()
12436                },
12437                push_options: None,
12438                packfile: Vec::new(),
12439            })
12440            .is_err()
12441        );
12442    }
12443
12444    #[test]
12445    fn receive_pack_report_status_parses_and_encodes_status_lines() {
12446        let frames = vec![
12447            PktLineFrame::Data(b"unpack ok\n".to_vec()),
12448            PktLineFrame::Data(b"ok refs/heads/main\n".to_vec()),
12449            PktLineFrame::Data(b"ng refs/heads/old non-fast-forward\n".to_vec()),
12450            PktLineFrame::Flush,
12451        ];
12452        let report =
12453            parse_receive_pack_report_status(&frames).expect("test operation should succeed");
12454        assert_eq!(
12455            report,
12456            ReceivePackReportStatus {
12457                unpack: ReceivePackUnpackStatus::Ok,
12458                commands: vec![
12459                    ReceivePackCommandStatus::Ok {
12460                        name: "refs/heads/main".into(),
12461                    },
12462                    ReceivePackCommandStatus::Ng {
12463                        name: "refs/heads/old".into(),
12464                        message: "non-fast-forward".into(),
12465                    },
12466                ],
12467            }
12468        );
12469        assert_eq!(
12470            encode_receive_pack_report_status(&report).expect("test operation should succeed"),
12471            frames
12472        );
12473
12474        let frames = vec![
12475            PktLineFrame::Data(b"unpack pack exceeds maximum size\n".to_vec()),
12476            PktLineFrame::Flush,
12477        ];
12478        assert_eq!(
12479            parse_receive_pack_report_status(&frames).expect("test operation should succeed"),
12480            ReceivePackReportStatus {
12481                unpack: ReceivePackUnpackStatus::Error("pack exceeds maximum size".into()),
12482                commands: Vec::new(),
12483            }
12484        );
12485    }
12486
12487    #[test]
12488    fn receive_pack_report_status_streams_round_trip() {
12489        let report = ReceivePackReportStatus {
12490            unpack: ReceivePackUnpackStatus::Ok,
12491            commands: vec![ReceivePackCommandStatus::Ok {
12492                name: "refs/heads/main".into(),
12493            }],
12494        };
12495        let mut encoded = Vec::new();
12496        write_receive_pack_report_status(&mut encoded, &report)
12497            .expect("test operation should succeed");
12498        encoded.extend_from_slice(b"tail");
12499
12500        let mut input = encoded.as_slice();
12501        assert_eq!(
12502            read_receive_pack_report_status(&mut input).expect("test operation should succeed"),
12503            report
12504        );
12505        assert_eq!(input, b"tail");
12506    }
12507
12508    #[test]
12509    fn receive_pack_report_status_rejects_malformed_status_lines() {
12510        assert!(parse_receive_pack_report_status(&[]).is_err());
12511        assert!(
12512            parse_receive_pack_report_status(&[
12513                PktLineFrame::Data(b"unpack ok\n".to_vec()),
12514                PktLineFrame::Data(b"ok refs/heads/main\n".to_vec()),
12515            ])
12516            .is_err()
12517        );
12518        assert!(
12519            parse_receive_pack_report_status(&[
12520                PktLineFrame::Flush,
12521                PktLineFrame::Data(b"ok refs/heads/main\n".to_vec()),
12522            ])
12523            .is_err()
12524        );
12525        assert!(
12526            parse_receive_pack_report_status(&[
12527                PktLineFrame::Data(b"unpack ok\n".to_vec()),
12528                PktLineFrame::Data(b"bad refs/heads/main\n".to_vec()),
12529                PktLineFrame::Flush,
12530            ])
12531            .is_err()
12532        );
12533        assert!(
12534            parse_receive_pack_report_status(&[
12535                PktLineFrame::Data(b"unpack ok\n".to_vec()),
12536                PktLineFrame::Data(b"ng refs/heads/main\n".to_vec()),
12537                PktLineFrame::Flush,
12538            ])
12539            .is_err()
12540        );
12541        assert!(
12542            encode_receive_pack_report_status(&ReceivePackReportStatus {
12543                unpack: ReceivePackUnpackStatus::Error("".into()),
12544                commands: Vec::new(),
12545            })
12546            .is_err()
12547        );
12548        assert!(
12549            encode_receive_pack_report_status(&ReceivePackReportStatus {
12550                unpack: ReceivePackUnpackStatus::Ok,
12551                commands: vec![ReceivePackCommandStatus::Ok {
12552                    name: "bad ref".into(),
12553                }],
12554            })
12555            .is_err()
12556        );
12557    }
12558
12559    #[test]
12560    fn receive_pack_report_status_v2_parses_and_encodes_options() {
12561        let old_oid = ObjectId::from_hex(
12562            ObjectFormat::Sha1,
12563            "1111111111111111111111111111111111111111",
12564        )
12565        .expect("test operation should succeed");
12566        let new_oid = ObjectId::from_hex(
12567            ObjectFormat::Sha1,
12568            "2222222222222222222222222222222222222222",
12569        )
12570        .expect("test operation should succeed");
12571        let frames = vec![
12572            PktLineFrame::Data(b"unpack ok\n".to_vec()),
12573            PktLineFrame::Data(b"ok refs/for/main\n".to_vec()),
12574            PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12575            PktLineFrame::Data(
12576                b"option old-oid 1111111111111111111111111111111111111111\n".to_vec(),
12577            ),
12578            PktLineFrame::Data(
12579                b"option new-oid 2222222222222222222222222222222222222222\n".to_vec(),
12580            ),
12581            PktLineFrame::Data(b"option forced-update\n".to_vec()),
12582            PktLineFrame::Data(b"ng refs/heads/old rejected by hook\n".to_vec()),
12583            PktLineFrame::Flush,
12584        ];
12585        let report = parse_receive_pack_report_status_v2(ObjectFormat::Sha1, &frames)
12586            .expect("test operation should succeed");
12587        assert_eq!(
12588            report,
12589            ReceivePackReportStatusV2 {
12590                unpack: ReceivePackUnpackStatus::Ok,
12591                commands: vec![
12592                    ReceivePackCommandStatusV2::Ok {
12593                        name: "refs/for/main".into(),
12594                        options: ReceivePackCommandStatusV2Options {
12595                            refname: Some("refs/heads/main".into()),
12596                            old_oid: Some(old_oid),
12597                            new_oid: Some(new_oid),
12598                            forced_update: true,
12599                        },
12600                    },
12601                    ReceivePackCommandStatusV2::Ng {
12602                        name: "refs/heads/old".into(),
12603                        message: "rejected by hook".into(),
12604                    },
12605                ],
12606            }
12607        );
12608        assert_eq!(
12609            encode_receive_pack_report_status_v2(&report).expect("test operation should succeed"),
12610            frames
12611        );
12612    }
12613
12614    #[test]
12615    fn receive_pack_report_status_v2_streams_round_trip() {
12616        let report = ReceivePackReportStatusV2 {
12617            unpack: ReceivePackUnpackStatus::Ok,
12618            commands: vec![ReceivePackCommandStatusV2::Ok {
12619                name: "refs/for/main".into(),
12620                options: ReceivePackCommandStatusV2Options {
12621                    refname: Some("refs/heads/main".into()),
12622                    old_oid: None,
12623                    new_oid: None,
12624                    forced_update: false,
12625                },
12626            }],
12627        };
12628        let mut encoded = Vec::new();
12629        write_receive_pack_report_status_v2(&mut encoded, &report)
12630            .expect("test operation should succeed");
12631        encoded.extend_from_slice(b"tail");
12632
12633        let mut input = encoded.as_slice();
12634        assert_eq!(
12635            read_receive_pack_report_status_v2(ObjectFormat::Sha1, &mut input)
12636                .expect("test operation should succeed"),
12637            report
12638        );
12639        assert_eq!(input, b"tail");
12640    }
12641
12642    #[test]
12643    fn receive_pack_report_status_v2_rejects_malformed_options() {
12644        assert!(parse_receive_pack_report_status_v2(ObjectFormat::Sha1, &[]).is_err());
12645        assert!(
12646            parse_receive_pack_report_status_v2(
12647                ObjectFormat::Sha1,
12648                &[
12649                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12650                    PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12651                    PktLineFrame::Flush,
12652                ],
12653            )
12654            .is_err()
12655        );
12656        assert!(
12657            parse_receive_pack_report_status_v2(
12658                ObjectFormat::Sha1,
12659                &[
12660                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12661                    PktLineFrame::Data(b"ng refs/heads/main rejected\n".to_vec()),
12662                    PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12663                    PktLineFrame::Flush,
12664                ],
12665            )
12666            .is_err()
12667        );
12668        assert!(
12669            parse_receive_pack_report_status_v2(
12670                ObjectFormat::Sha1,
12671                &[
12672                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12673                    PktLineFrame::Data(b"ok refs/for/main\n".to_vec()),
12674                    PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12675                    PktLineFrame::Data(b"option refname refs/heads/next\n".to_vec()),
12676                    PktLineFrame::Flush,
12677                ],
12678            )
12679            .is_err()
12680        );
12681        assert!(
12682            parse_receive_pack_report_status_v2(
12683                ObjectFormat::Sha1,
12684                &[
12685                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12686                    PktLineFrame::Data(b"ok refs/for/main\n".to_vec()),
12687                    PktLineFrame::Data(b"option old-oid not-an-oid\n".to_vec()),
12688                    PktLineFrame::Flush,
12689                ],
12690            )
12691            .is_err()
12692        );
12693        assert!(
12694            encode_receive_pack_report_status_v2(&ReceivePackReportStatusV2 {
12695                unpack: ReceivePackUnpackStatus::Ok,
12696                commands: vec![ReceivePackCommandStatusV2::Ok {
12697                    name: "refs/for/main".into(),
12698                    options: ReceivePackCommandStatusV2Options {
12699                        refname: Some("bad ref".into()),
12700                        ..ReceivePackCommandStatusV2Options::default()
12701                    },
12702                }],
12703            })
12704            .is_err()
12705        );
12706    }
12707
12708    #[test]
12709    fn receive_pack_push_options_parse_and_encode_options() {
12710        let frames = vec![
12711            PktLineFrame::Data(b"ci.skip\n".to_vec()),
12712            PktLineFrame::Data(b"deploy target=staging\n".to_vec()),
12713            PktLineFrame::Data(b"\n".to_vec()),
12714            PktLineFrame::Flush,
12715        ];
12716        let options =
12717            parse_receive_pack_push_options(&frames).expect("test operation should succeed");
12718        assert_eq!(
12719            options,
12720            vec![
12721                "ci.skip".to_string(),
12722                "deploy target=staging".to_string(),
12723                String::new(),
12724            ]
12725        );
12726        assert_eq!(
12727            encode_receive_pack_push_options(&options).expect("test operation should succeed"),
12728            frames
12729        );
12730        assert_eq!(
12731            parse_receive_pack_push_options(&[PktLineFrame::Flush])
12732                .expect("test operation should succeed"),
12733            Vec::<String>::new()
12734        );
12735    }
12736
12737    #[test]
12738    fn receive_pack_push_options_streams_round_trip() {
12739        let options = vec!["ci.skip".to_string(), "reviewer=alice".to_string()];
12740        let mut encoded = Vec::new();
12741        write_receive_pack_push_options(&mut encoded, &options)
12742            .expect("test operation should succeed");
12743        encoded.extend_from_slice(b"PACK");
12744
12745        let mut input = encoded.as_slice();
12746        assert_eq!(
12747            read_receive_pack_push_options(&mut input).expect("test operation should succeed"),
12748            options
12749        );
12750        assert_eq!(input, b"PACK");
12751    }
12752
12753    #[test]
12754    fn receive_pack_push_options_reject_malformed_streams() {
12755        assert!(
12756            parse_receive_pack_push_options(&[PktLineFrame::Data(b"ci.skip\n".to_vec())]).is_err()
12757        );
12758        assert!(
12759            parse_receive_pack_push_options(&[PktLineFrame::Delimiter, PktLineFrame::Flush])
12760                .is_err()
12761        );
12762        assert!(
12763            parse_receive_pack_push_options(&[
12764                PktLineFrame::Data(b"ci.skip\n".to_vec()),
12765                PktLineFrame::Flush,
12766                PktLineFrame::Data(b"after\n".to_vec()),
12767            ])
12768            .is_err()
12769        );
12770        assert!(
12771            parse_receive_pack_push_options(&[
12772                PktLineFrame::Data(b"bad\0option\n".to_vec()),
12773                PktLineFrame::Flush,
12774            ])
12775            .is_err()
12776        );
12777        assert!(encode_receive_pack_push_options(&["bad\noption".to_string()]).is_err());
12778    }
12779
12780    #[test]
12781    fn protocol_v2_advertisement_parses_version_and_capabilities() {
12782        let frames = parse_pkt_line_stream(
12783            b"000eversion 2\n0015agent=git/2.54.0\n0013ls-refs=unborn\n0027fetch=shallow wait-for-done filter\n0012server-option\n0000",
12784        )
12785        .expect("test operation should succeed");
12786        let handshake =
12787            parse_protocol_v2_advertisement(&frames).expect("test operation should succeed");
12788        assert_eq!(handshake.protocol, ProtocolVersion::V2);
12789        assert_eq!(
12790            handshake.capabilities,
12791            vec![
12792                Capability {
12793                    name: "agent".into(),
12794                    value: Some("git/2.54.0".into()),
12795                },
12796                Capability {
12797                    name: "ls-refs".into(),
12798                    value: Some("unborn".into()),
12799                },
12800                Capability {
12801                    name: "fetch".into(),
12802                    value: Some("shallow wait-for-done filter".into()),
12803                },
12804                Capability {
12805                    name: "server-option".into(),
12806                    value: None,
12807                },
12808            ]
12809        );
12810        assert_eq!(
12811            encode_protocol_v2_advertisement(&handshake).expect("test operation should succeed"),
12812            frames
12813        );
12814    }
12815
12816    #[test]
12817    fn protocol_v2_advertisement_reads_until_flush() {
12818        let mut input = b"000eversion 2\n0013ls-refs=unborn\n0000next-session".as_slice();
12819        let handshake =
12820            read_protocol_v2_advertisement(&mut input).expect("test operation should succeed");
12821        assert_eq!(handshake.protocol, ProtocolVersion::V2);
12822        assert_eq!(
12823            handshake.capabilities,
12824            vec![Capability {
12825                name: "ls-refs".into(),
12826                value: Some("unborn".into()),
12827            }]
12828        );
12829        assert_eq!(input, b"next-session");
12830    }
12831
12832    #[test]
12833    fn protocol_v2_advertisement_writes_stream() {
12834        let handshake = TransportHandshake {
12835            protocol: ProtocolVersion::V2,
12836            capabilities: vec![
12837                Capability {
12838                    name: "agent".into(),
12839                    value: Some("sley/0".into()),
12840                },
12841                Capability {
12842                    name: "fetch".into(),
12843                    value: Some("shallow filter".into()),
12844                },
12845            ],
12846        };
12847        let mut encoded = Vec::new();
12848        write_protocol_v2_advertisement(&mut encoded, &handshake)
12849            .expect("test operation should succeed");
12850        let mut input = encoded.as_slice();
12851        assert_eq!(
12852            read_protocol_v2_advertisement(&mut input).expect("test operation should succeed"),
12853            handshake
12854        );
12855        assert!(input.is_empty());
12856        assert!(
12857            encode_protocol_v2_advertisement(&TransportHandshake {
12858                protocol: ProtocolVersion::V1,
12859                capabilities: Vec::new(),
12860            })
12861            .is_err()
12862        );
12863    }
12864
12865    #[test]
12866    fn protocol_v2_advertisement_rejects_malformed_sequences() {
12867        assert!(parse_protocol_v2_advertisement(&[]).is_err());
12868        assert!(
12869            parse_protocol_v2_advertisement(&[
12870                PktLineFrame::Data(b"version 1\n".to_vec()),
12871                PktLineFrame::Flush,
12872            ])
12873            .is_err()
12874        );
12875        assert!(
12876            parse_protocol_v2_advertisement(&[PktLineFrame::Data(b"version 2\n".to_vec())])
12877                .is_err()
12878        );
12879        assert!(
12880            parse_protocol_v2_advertisement(&[
12881                PktLineFrame::Data(b"version 2\n".to_vec()),
12882                PktLineFrame::Delimiter,
12883            ])
12884            .is_err()
12885        );
12886        assert!(
12887            parse_protocol_v2_advertisement(&[
12888                PktLineFrame::Data(b"version 2\n".to_vec()),
12889                PktLineFrame::Data(b"fetch=\n".to_vec()),
12890                PktLineFrame::Flush,
12891            ])
12892            .is_err()
12893        );
12894    }
12895
12896    #[test]
12897    fn protocol_v2_command_request_parses_and_encodes_sections() {
12898        let frames = parse_pkt_line_stream(
12899            b"0014command=ls-refs\n0011agent=sley/0\n0017object-format=sha1\n00010009peel\n000csymrefs\n001bref-prefix refs/heads/\n0000",
12900        )
12901        .expect("test operation should succeed");
12902        let request =
12903            parse_protocol_v2_command_request(&frames).expect("test operation should succeed");
12904        assert_eq!(
12905            request,
12906            ProtocolV2CommandRequest {
12907                command: "ls-refs".into(),
12908                capabilities: vec![
12909                    Capability {
12910                        name: "agent".into(),
12911                        value: Some("sley/0".into()),
12912                    },
12913                    Capability {
12914                        name: "object-format".into(),
12915                        value: Some("sha1".into()),
12916                    },
12917                ],
12918                arguments: vec![
12919                    b"peel".to_vec(),
12920                    b"symrefs".to_vec(),
12921                    b"ref-prefix refs/heads/".to_vec(),
12922                ],
12923            }
12924        );
12925        assert_eq!(
12926            encode_protocol_v2_command_request(&request).expect("test operation should succeed"),
12927            frames
12928        );
12929    }
12930
12931    #[test]
12932    fn protocol_v2_command_request_allows_no_argument_section() {
12933        let frames = parse_pkt_line_stream(b"0012command=fetch\n0000")
12934            .expect("test operation should succeed");
12935        let request =
12936            parse_protocol_v2_command_request(&frames).expect("test operation should succeed");
12937        assert_eq!(
12938            request,
12939            ProtocolV2CommandRequest {
12940                command: "fetch".into(),
12941                capabilities: Vec::new(),
12942                arguments: Vec::new(),
12943            }
12944        );
12945        assert_eq!(
12946            encode_protocol_v2_command_request(&request).expect("test operation should succeed"),
12947            frames
12948        );
12949    }
12950
12951    #[test]
12952    fn protocol_v2_request_parses_commands_and_empty_done() {
12953        let frames = parse_pkt_line_stream(b"0012command=fetch\n0000")
12954            .expect("test operation should succeed");
12955        let command = ProtocolV2CommandRequest {
12956            command: "fetch".into(),
12957            capabilities: Vec::new(),
12958            arguments: Vec::new(),
12959        };
12960        assert_eq!(
12961            parse_protocol_v2_request(&frames).expect("test operation should succeed"),
12962            ProtocolV2Request::Command(command.clone())
12963        );
12964        assert_eq!(
12965            encode_protocol_v2_request(&ProtocolV2Request::Command(command))
12966                .expect("test operation should succeed"),
12967            frames
12968        );
12969
12970        assert_eq!(
12971            parse_protocol_v2_request(&[PktLineFrame::Flush])
12972                .expect("test operation should succeed"),
12973            ProtocolV2Request::Done
12974        );
12975        assert_eq!(
12976            encode_protocol_v2_request(&ProtocolV2Request::Done)
12977                .expect("test operation should succeed"),
12978            vec![PktLineFrame::Flush]
12979        );
12980    }
12981
12982    #[test]
12983    fn protocol_v2_request_streams_empty_done() {
12984        let mut encoded = Vec::new();
12985        write_protocol_v2_request(&mut encoded, &ProtocolV2Request::Done)
12986            .expect("test operation should succeed");
12987        encoded.extend_from_slice(b"tail");
12988
12989        let mut input = encoded.as_slice();
12990        assert_eq!(
12991            read_protocol_v2_request(&mut input).expect("test operation should succeed"),
12992            ProtocolV2Request::Done
12993        );
12994        assert_eq!(input, b"tail");
12995        let mut command_input = encoded.as_slice();
12996        assert!(read_protocol_v2_command_request(&mut command_input).is_err());
12997    }
12998
12999    #[test]
13000    fn protocol_v2_command_request_streams_round_trip() {
13001        let request = ProtocolV2CommandRequest {
13002            command: "ls-refs".into(),
13003            capabilities: vec![Capability {
13004                name: "agent".into(),
13005                value: Some("sley/0".into()),
13006            }],
13007            arguments: vec![b"peel".to_vec(), b"symrefs".to_vec()],
13008        };
13009        let mut encoded = Vec::new();
13010        write_protocol_v2_command_request(&mut encoded, &request)
13011            .expect("test operation should succeed");
13012        encoded.extend_from_slice(b"tail");
13013
13014        let mut input = encoded.as_slice();
13015        assert_eq!(
13016            read_protocol_v2_command_request(&mut input).expect("test operation should succeed"),
13017            request
13018        );
13019        assert_eq!(input, b"tail");
13020    }
13021
13022    #[test]
13023    fn protocol_v2_command_request_rejects_malformed_sequences() {
13024        assert!(parse_protocol_v2_command_request(&[]).is_err());
13025        assert!(
13026            parse_protocol_v2_command_request(&[
13027                PktLineFrame::Data(b"agent=sley/0\n".to_vec()),
13028                PktLineFrame::Flush,
13029            ])
13030            .is_err()
13031        );
13032        assert!(
13033            parse_protocol_v2_command_request(&[
13034                PktLineFrame::Data(b"command=ls-refs\n".to_vec()),
13035                PktLineFrame::Delimiter,
13036                PktLineFrame::Delimiter,
13037                PktLineFrame::Flush,
13038            ])
13039            .is_err()
13040        );
13041        assert!(
13042            parse_protocol_v2_command_request(&[
13043                PktLineFrame::Data(b"command=ls-refs\n".to_vec()),
13044                PktLineFrame::Delimiter,
13045                PktLineFrame::Data(b"\n".to_vec()),
13046                PktLineFrame::Flush,
13047            ])
13048            .is_err()
13049        );
13050        assert!(
13051            encode_protocol_v2_command_request(&ProtocolV2CommandRequest {
13052                command: "bad command".into(),
13053                capabilities: Vec::new(),
13054                arguments: Vec::new(),
13055            })
13056            .is_err()
13057        );
13058    }
13059
13060    #[test]
13061    fn protocol_v2_ls_refs_request_parses_and_encodes_arguments() {
13062        let command = ProtocolV2CommandRequest {
13063            command: "ls-refs".into(),
13064            capabilities: Vec::new(),
13065            arguments: vec![
13066                b"peel".to_vec(),
13067                b"symrefs".to_vec(),
13068                b"unborn".to_vec(),
13069                b"ref-prefix HEAD".to_vec(),
13070                b"ref-prefix refs/heads/".to_vec(),
13071            ],
13072        };
13073        let request = ProtocolV2LsRefsRequest::from_command_request(&command)
13074            .expect("test operation should succeed");
13075        assert_eq!(
13076            request,
13077            ProtocolV2LsRefsRequest {
13078                peel: true,
13079                symrefs: true,
13080                unborn: true,
13081                ref_prefixes: vec!["HEAD".into(), "refs/heads/".into()],
13082            }
13083        );
13084        assert_eq!(
13085            request
13086                .to_command_request()
13087                .expect("test operation should succeed"),
13088            command
13089        );
13090        assert!(
13091            ProtocolV2LsRefsRequest::from_command_request(&ProtocolV2CommandRequest {
13092                command: "fetch".into(),
13093                capabilities: Vec::new(),
13094                arguments: Vec::new(),
13095            })
13096            .is_err()
13097        );
13098        assert!(
13099            ProtocolV2LsRefsRequest::from_command_request(&ProtocolV2CommandRequest {
13100                command: "ls-refs".into(),
13101                capabilities: Vec::new(),
13102                arguments: vec![b"ref-prefix ".to_vec()],
13103            })
13104            .is_err()
13105        );
13106    }
13107
13108    #[test]
13109    fn protocol_v2_ls_refs_request_streams_round_trip() {
13110        let request = ProtocolV2LsRefsRequest {
13111            peel: true,
13112            symrefs: true,
13113            unborn: false,
13114            ref_prefixes: vec!["HEAD".into(), "refs/tags/".into()],
13115        };
13116        let mut encoded = Vec::new();
13117        write_protocol_v2_ls_refs_request(&mut encoded, &request)
13118            .expect("test operation should succeed");
13119        encoded.extend_from_slice(b"tail");
13120
13121        let mut input = encoded.as_slice();
13122        assert_eq!(
13123            read_protocol_v2_ls_refs_request(&mut input).expect("test operation should succeed"),
13124            request
13125        );
13126        assert_eq!(input, b"tail");
13127    }
13128
13129    #[test]
13130    fn protocol_v2_ls_refs_response_parses_and_encodes_records() {
13131        let oid = ObjectId::from_hex(
13132            ObjectFormat::Sha1,
13133            "1111111111111111111111111111111111111111",
13134        )
13135        .expect("test operation should succeed");
13136        let peeled = ObjectId::from_hex(
13137            ObjectFormat::Sha1,
13138            "2222222222222222222222222222222222222222",
13139        )
13140        .expect("test operation should succeed");
13141        let frames = vec![
13142            PktLineFrame::Data(
13143                b"1111111111111111111111111111111111111111 refs/tags/v1 peeled:2222222222222222222222222222222222222222 symref-target:refs/heads/main custom\n"
13144                    .to_vec(),
13145            ),
13146            PktLineFrame::Data(b"unborn HEAD symref-target:refs/heads/main\n".to_vec()),
13147            PktLineFrame::Flush,
13148        ];
13149        let records = parse_protocol_v2_ls_refs_response(ObjectFormat::Sha1, &frames)
13150            .expect("test operation should succeed");
13151        assert_eq!(
13152            records,
13153            vec![
13154                ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
13155                    oid,
13156                    name: "refs/tags/v1".into(),
13157                    peeled: Some(peeled),
13158                    symref_target: Some("refs/heads/main".into()),
13159                    attributes: vec!["custom".into()],
13160                }),
13161                ProtocolV2LsRefsRecord::Unborn {
13162                    name: "HEAD".into(),
13163                    symref_target: Some("refs/heads/main".into()),
13164                    attributes: Vec::new(),
13165                },
13166            ]
13167        );
13168        assert_eq!(
13169            encode_protocol_v2_ls_refs_response(&records).expect("test operation should succeed"),
13170            frames
13171        );
13172    }
13173
13174    #[test]
13175    fn protocol_v2_ls_refs_response_streams_round_trip() {
13176        let oid = ObjectId::from_hex(
13177            ObjectFormat::Sha1,
13178            "1111111111111111111111111111111111111111",
13179        )
13180        .expect("test operation should succeed");
13181        let records = vec![ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
13182            oid,
13183            name: "refs/heads/main".into(),
13184            peeled: None,
13185            symref_target: Some("refs/heads/trunk".into()),
13186            attributes: vec!["custom".into()],
13187        })];
13188        let mut encoded = Vec::new();
13189        write_protocol_v2_ls_refs_response(&mut encoded, &records)
13190            .expect("test operation should succeed");
13191        encoded.extend_from_slice(b"tail");
13192
13193        let mut input = encoded.as_slice();
13194        assert_eq!(
13195            read_protocol_v2_ls_refs_response(ObjectFormat::Sha1, &mut input)
13196                .expect("test operation should succeed"),
13197            records
13198        );
13199        assert_eq!(input, b"tail");
13200    }
13201
13202    #[test]
13203    fn protocol_v2_ls_refs_response_reads_stateless_response_end() {
13204        let oid = ObjectId::from_hex(
13205            ObjectFormat::Sha1,
13206            "1111111111111111111111111111111111111111",
13207        )
13208        .expect("test operation should succeed");
13209        let records = vec![ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
13210            oid,
13211            name: "refs/heads/main".into(),
13212            peeled: None,
13213            symref_target: None,
13214            attributes: Vec::new(),
13215        })];
13216        let mut encoded = Vec::new();
13217        write_protocol_v2_ls_refs_response_with_response_end(&mut encoded, &records)
13218            .expect("test operation should succeed");
13219        encoded.extend_from_slice(b"tail");
13220
13221        let mut input = encoded.as_slice();
13222        assert_eq!(
13223            read_protocol_v2_ls_refs_response_until_response_end(ObjectFormat::Sha1, &mut input)
13224                .expect("test operation should succeed"),
13225            records
13226        );
13227        assert_eq!(input, b"tail");
13228        assert!(
13229            parse_protocol_v2_ls_refs_response(
13230                ObjectFormat::Sha1,
13231                &[
13232                    PktLineFrame::Data(
13233                        b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec()
13234                    ),
13235                    PktLineFrame::ResponseEnd
13236                ],
13237            )
13238            .is_err()
13239        );
13240    }
13241
13242    #[test]
13243    fn protocol_v2_ls_refs_exchange_writes_request_and_reads_response() {
13244        let oid = ObjectId::from_hex(
13245            ObjectFormat::Sha1,
13246            "1111111111111111111111111111111111111111",
13247        )
13248        .expect("test operation should succeed");
13249        let request = ProtocolV2LsRefsRequest {
13250            peel: true,
13251            symrefs: true,
13252            unborn: false,
13253            ref_prefixes: vec!["refs/heads/".into()],
13254        };
13255        let records = vec![ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
13256            oid,
13257            name: "refs/heads/main".into(),
13258            peeled: None,
13259            symref_target: None,
13260            attributes: Vec::new(),
13261        })];
13262        let mut response = Vec::new();
13263        write_protocol_v2_ls_refs_response(&mut response, &records)
13264            .expect("test operation should succeed");
13265
13266        let mut input = response.as_slice();
13267        let mut output = Vec::new();
13268        assert_eq!(
13269            exchange_protocol_v2_ls_refs(ObjectFormat::Sha1, &mut input, &mut output, &request)
13270                .expect("test operation should succeed"),
13271            records
13272        );
13273        assert!(input.is_empty());
13274        let mut output_read = output.as_slice();
13275        assert_eq!(
13276            read_protocol_v2_ls_refs_request(&mut output_read)
13277                .expect("test operation should succeed"),
13278            request
13279        );
13280    }
13281
13282    #[test]
13283    fn protocol_v2_ls_refs_response_rejects_malformed_records() {
13284        assert!(
13285            parse_protocol_v2_ls_refs_response(
13286                ObjectFormat::Sha1,
13287                &[PktLineFrame::Data(
13288                    b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec()
13289                )],
13290            )
13291            .is_err()
13292        );
13293        assert!(
13294            parse_protocol_v2_ls_refs_response(
13295                ObjectFormat::Sha1,
13296                &[
13297                    PktLineFrame::Data(
13298                        b"1111111111111111111111111111111111111111 refs/heads/main peeled:2222222222222222222222222222222222222222 peeled:3333333333333333333333333333333333333333\n"
13299                            .to_vec()
13300                    ),
13301                    PktLineFrame::Flush,
13302                ],
13303            )
13304            .is_err()
13305        );
13306        assert!(
13307            parse_protocol_v2_ls_refs_response(
13308                ObjectFormat::Sha1,
13309                &[
13310                    PktLineFrame::Data(
13311                        b"unborn HEAD peeled:2222222222222222222222222222222222222222\n".to_vec()
13312                    ),
13313                    PktLineFrame::Flush,
13314                ],
13315            )
13316            .is_err()
13317        );
13318        assert!(
13319            encode_protocol_v2_ls_refs_response(&[ProtocolV2LsRefsRecord::Ref(
13320                ProtocolV2LsRefsRef {
13321                    oid: ObjectId::from_hex(
13322                        ObjectFormat::Sha1,
13323                        "1111111111111111111111111111111111111111",
13324                    )
13325                    .expect("test operation should succeed"),
13326                    name: "refs/heads/main".into(),
13327                    peeled: None,
13328                    symref_target: None,
13329                    attributes: vec!["peeled:2222222222222222222222222222222222222222".into()],
13330                }
13331            )])
13332            .is_err()
13333        );
13334    }
13335
13336    #[test]
13337    fn protocol_v2_fetch_request_parses_and_encodes_arguments() {
13338        let want = ObjectId::from_hex(
13339            ObjectFormat::Sha1,
13340            "1111111111111111111111111111111111111111",
13341        )
13342        .expect("test operation should succeed");
13343        let have = ObjectId::from_hex(
13344            ObjectFormat::Sha1,
13345            "2222222222222222222222222222222222222222",
13346        )
13347        .expect("test operation should succeed");
13348        let shallow = ObjectId::from_hex(
13349            ObjectFormat::Sha1,
13350            "3333333333333333333333333333333333333333",
13351        )
13352        .expect("test operation should succeed");
13353        let command = ProtocolV2CommandRequest {
13354            command: "fetch".into(),
13355            capabilities: Vec::new(),
13356            arguments: vec![
13357                b"want 1111111111111111111111111111111111111111".to_vec(),
13358                b"want-ref refs/heads/main".to_vec(),
13359                b"have 2222222222222222222222222222222222222222".to_vec(),
13360                b"shallow 3333333333333333333333333333333333333333".to_vec(),
13361                b"deepen 10".to_vec(),
13362                b"deepen-since 123456789".to_vec(),
13363                b"deepen-not refs/tags/v1".to_vec(),
13364                b"deepen-relative".to_vec(),
13365                b"filter blob:none".to_vec(),
13366                b"packfile-uris http,https".to_vec(),
13367                b"thin-pack".to_vec(),
13368                b"no-progress".to_vec(),
13369                b"include-tag".to_vec(),
13370                b"ofs-delta".to_vec(),
13371                b"sideband-all".to_vec(),
13372                b"wait-for-done".to_vec(),
13373                b"done".to_vec(),
13374            ],
13375        };
13376        let request = ProtocolV2FetchRequest::from_command_request(ObjectFormat::Sha1, &command)
13377            .expect("test operation should succeed");
13378        assert_eq!(
13379            request,
13380            ProtocolV2FetchRequest {
13381                wants: vec![want],
13382                want_refs: vec!["refs/heads/main".into()],
13383                haves: vec![have],
13384                shallow: vec![shallow],
13385                deepen: Some(10),
13386                deepen_since: Some(123456789),
13387                deepen_not: vec!["refs/tags/v1".into()],
13388                deepen_relative: true,
13389                filter: Some("blob:none".into()),
13390                packfile_uris: Some("http,https".into()),
13391                thin_pack: true,
13392                no_progress: true,
13393                include_tag: true,
13394                ofs_delta: true,
13395                sideband_all: true,
13396                wait_for_done: true,
13397                done: true,
13398            }
13399        );
13400        assert_eq!(
13401            request
13402                .to_command_request()
13403                .expect("test operation should succeed"),
13404            command
13405        );
13406    }
13407
13408    #[test]
13409    fn protocol_v2_fetch_request_rejects_malformed_arguments() {
13410        assert!(
13411            ProtocolV2FetchRequest::from_command_request(
13412                ObjectFormat::Sha1,
13413                &ProtocolV2CommandRequest {
13414                    command: "ls-refs".into(),
13415                    capabilities: Vec::new(),
13416                    arguments: Vec::new(),
13417                },
13418            )
13419            .is_err()
13420        );
13421        assert!(
13422            ProtocolV2FetchRequest::from_command_request(
13423                ObjectFormat::Sha1,
13424                &ProtocolV2CommandRequest {
13425                    command: "fetch".into(),
13426                    capabilities: Vec::new(),
13427                    arguments: vec![b"want not-an-oid".to_vec()],
13428                },
13429            )
13430            .is_err()
13431        );
13432        assert!(
13433            ProtocolV2FetchRequest::from_command_request(
13434                ObjectFormat::Sha1,
13435                &ProtocolV2CommandRequest {
13436                    command: "fetch".into(),
13437                    capabilities: Vec::new(),
13438                    arguments: vec![b"deepen 0".to_vec()],
13439                },
13440            )
13441            .is_err()
13442        );
13443        assert!(
13444            ProtocolV2FetchRequest::from_command_request(
13445                ObjectFormat::Sha1,
13446                &ProtocolV2CommandRequest {
13447                    command: "fetch".into(),
13448                    capabilities: Vec::new(),
13449                    arguments: vec![b"filter blob:none".to_vec(), b"filter tree:0".to_vec()],
13450                },
13451            )
13452            .is_err()
13453        );
13454        assert!(
13455            ProtocolV2FetchRequest {
13456                deepen: Some(0),
13457                ..ProtocolV2FetchRequest::default()
13458            }
13459            .to_command_request()
13460            .is_err()
13461        );
13462    }
13463
13464    #[test]
13465    fn protocol_v2_fetch_request_streams_round_trip() {
13466        let want = ObjectId::from_hex(
13467            ObjectFormat::Sha1,
13468            "1111111111111111111111111111111111111111",
13469        )
13470        .expect("test operation should succeed");
13471        let have = ObjectId::from_hex(
13472            ObjectFormat::Sha1,
13473            "2222222222222222222222222222222222222222",
13474        )
13475        .expect("test operation should succeed");
13476        let request = ProtocolV2FetchRequest {
13477            wants: vec![want],
13478            haves: vec![have],
13479            deepen: Some(5),
13480            filter: Some("blob:none".into()),
13481            thin_pack: true,
13482            done: true,
13483            ..ProtocolV2FetchRequest::default()
13484        };
13485        let mut encoded = Vec::new();
13486        write_protocol_v2_fetch_request(&mut encoded, &request)
13487            .expect("test operation should succeed");
13488        encoded.extend_from_slice(b"tail");
13489
13490        let mut input = encoded.as_slice();
13491        assert_eq!(
13492            read_protocol_v2_fetch_request(ObjectFormat::Sha1, &mut input)
13493                .expect("test operation should succeed"),
13494            request
13495        );
13496        assert_eq!(input, b"tail");
13497    }
13498
13499    #[test]
13500    fn protocol_v2_fetch_response_parses_and_encodes_sections() {
13501        let ack = ObjectId::from_hex(
13502            ObjectFormat::Sha1,
13503            "1111111111111111111111111111111111111111",
13504        )
13505        .expect("test operation should succeed");
13506        let shallow = ObjectId::from_hex(
13507            ObjectFormat::Sha1,
13508            "2222222222222222222222222222222222222222",
13509        )
13510        .expect("test operation should succeed");
13511        let wanted = ObjectId::from_hex(
13512            ObjectFormat::Sha1,
13513            "3333333333333333333333333333333333333333",
13514        )
13515        .expect("test operation should succeed");
13516        let pack_hash = ObjectId::from_hex(
13517            ObjectFormat::Sha1,
13518            "4444444444444444444444444444444444444444",
13519        )
13520        .expect("test operation should succeed");
13521        let frames = vec![
13522            PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13523            PktLineFrame::Data(b"ACK 1111111111111111111111111111111111111111\n".to_vec()),
13524            PktLineFrame::Data(b"ready\n".to_vec()),
13525            PktLineFrame::Delimiter,
13526            PktLineFrame::Data(b"shallow-info\n".to_vec()),
13527            PktLineFrame::Data(b"shallow 2222222222222222222222222222222222222222\n".to_vec()),
13528            PktLineFrame::Delimiter,
13529            PktLineFrame::Data(b"wanted-refs\n".to_vec()),
13530            PktLineFrame::Data(
13531                b"3333333333333333333333333333333333333333 refs/heads/main\n".to_vec(),
13532            ),
13533            PktLineFrame::Delimiter,
13534            PktLineFrame::Data(b"packfile-uris\n".to_vec()),
13535            PktLineFrame::Data(
13536                b"4444444444444444444444444444444444444444 https://example.invalid/pack-a.pack\n"
13537                    .to_vec(),
13538            ),
13539            PktLineFrame::Delimiter,
13540            PktLineFrame::Data(b"packfile\n".to_vec()),
13541            PktLineFrame::Data(b"\x01PACK bytes".to_vec()),
13542            PktLineFrame::Flush,
13543        ];
13544        let sections = parse_protocol_v2_fetch_response(ObjectFormat::Sha1, &frames)
13545            .expect("test operation should succeed");
13546        assert_eq!(
13547            sections,
13548            vec![
13549                ProtocolV2FetchResponseSection::Acknowledgments(vec![
13550                    ProtocolV2FetchAcknowledgment::Ack(ack),
13551                    ProtocolV2FetchAcknowledgment::Ready,
13552                ]),
13553                ProtocolV2FetchResponseSection::ShallowInfo(vec![
13554                    ProtocolV2FetchShallowInfo::Shallow(shallow)
13555                ]),
13556                ProtocolV2FetchResponseSection::WantedRefs(vec![ProtocolV2FetchWantedRef {
13557                    oid: wanted,
13558                    name: "refs/heads/main".into(),
13559                }]),
13560                ProtocolV2FetchResponseSection::PackfileUris(vec![ProtocolV2FetchPackfileUri {
13561                    pack_hash,
13562                    uri: "https://example.invalid/pack-a.pack".into(),
13563                }]),
13564                ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK bytes".to_vec()]),
13565            ]
13566        );
13567        assert_eq!(
13568            encode_protocol_v2_fetch_response(&sections).expect("test operation should succeed"),
13569            frames
13570        );
13571    }
13572
13573    #[test]
13574    fn protocol_v2_fetch_response_preserves_unknown_sections() {
13575        let frames = vec![
13576            PktLineFrame::Data(b"server-feature\n".to_vec()),
13577            PktLineFrame::Data(b"opaque line\n".to_vec()),
13578            PktLineFrame::Flush,
13579        ];
13580        let sections = parse_protocol_v2_fetch_response(ObjectFormat::Sha1, &frames)
13581            .expect("test operation should succeed");
13582        assert_eq!(
13583            sections,
13584            vec![ProtocolV2FetchResponseSection::Unknown {
13585                name: "server-feature".into(),
13586                lines: vec![b"opaque line\n".to_vec()],
13587            }]
13588        );
13589        assert_eq!(
13590            encode_protocol_v2_fetch_response(&sections).expect("test operation should succeed"),
13591            frames
13592        );
13593    }
13594
13595    #[test]
13596    fn protocol_v2_fetch_response_streams_round_trip() {
13597        let ack = ObjectId::from_hex(
13598            ObjectFormat::Sha1,
13599            "1111111111111111111111111111111111111111",
13600        )
13601        .expect("test operation should succeed");
13602        let sections = vec![
13603            ProtocolV2FetchResponseSection::Acknowledgments(vec![
13604                ProtocolV2FetchAcknowledgment::Ack(ack),
13605                ProtocolV2FetchAcknowledgment::Ready,
13606            ]),
13607            ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK bytes".to_vec()]),
13608        ];
13609        let mut encoded = Vec::new();
13610        write_protocol_v2_fetch_response(&mut encoded, &sections)
13611            .expect("test operation should succeed");
13612        encoded.extend_from_slice(b"tail");
13613
13614        let mut input = encoded.as_slice();
13615        assert_eq!(
13616            read_protocol_v2_fetch_response(ObjectFormat::Sha1, &mut input)
13617                .expect("test operation should succeed"),
13618            sections
13619        );
13620        assert_eq!(input, b"tail");
13621    }
13622
13623    #[test]
13624    fn protocol_v2_fetch_sideband_all_response_parses_sections_and_progress() {
13625        let frames = vec![
13626            PktLineFrame::Data(
13627                encode_sideband_packet(&SideBandPacket {
13628                    channel: SideBandChannel::Data,
13629                    data: b"acknowledgments\n".to_vec(),
13630                })
13631                .expect("test operation should succeed"),
13632            ),
13633            PktLineFrame::Data(
13634                encode_sideband_packet(&SideBandPacket {
13635                    channel: SideBandChannel::Data,
13636                    data: b"NAK\n".to_vec(),
13637                })
13638                .expect("test operation should succeed"),
13639            ),
13640            PktLineFrame::Data(
13641                encode_sideband_packet(&SideBandPacket {
13642                    channel: SideBandChannel::Progress,
13643                    data: b"keepalive\n".to_vec(),
13644                })
13645                .expect("test operation should succeed"),
13646            ),
13647            PktLineFrame::Delimiter,
13648            PktLineFrame::Data(
13649                encode_sideband_packet(&SideBandPacket {
13650                    channel: SideBandChannel::Data,
13651                    data: b"packfile\n".to_vec(),
13652                })
13653                .expect("test operation should succeed"),
13654            ),
13655            PktLineFrame::Data(b"\x01PACK".to_vec()),
13656            PktLineFrame::Data(b"\x02counting objects\n".to_vec()),
13657            PktLineFrame::Flush,
13658        ];
13659
13660        let response = parse_protocol_v2_fetch_sideband_all_response(ObjectFormat::Sha1, &frames)
13661            .expect("test operation should succeed");
13662        assert_eq!(
13663            response,
13664            ProtocolV2FetchSidebandAllResponse {
13665                sections: vec![
13666                    ProtocolV2FetchResponseSection::Acknowledgments(vec![
13667                        ProtocolV2FetchAcknowledgment::Nak
13668                    ]),
13669                    ProtocolV2FetchResponseSection::Packfile(vec![
13670                        b"\x01PACK".to_vec(),
13671                        b"\x02counting objects\n".to_vec(),
13672                    ]),
13673                ],
13674                progress: vec![b"keepalive\n".to_vec()],
13675            }
13676        );
13677        assert_eq!(
13678            demux_protocol_v2_fetch_packfile(&response.sections)
13679                .expect("test operation should succeed"),
13680            Some(SideBandDemux {
13681                data: b"PACK".to_vec(),
13682                progress: vec![b"counting objects\n".to_vec()],
13683            })
13684        );
13685    }
13686
13687    #[test]
13688    fn protocol_v2_fetch_sideband_all_response_streams_round_trip() {
13689        let sections = vec![
13690            ProtocolV2FetchResponseSection::Acknowledgments(vec![
13691                ProtocolV2FetchAcknowledgment::Nak,
13692            ]),
13693            ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK bytes".to_vec()]),
13694        ];
13695        let mut encoded = Vec::new();
13696        write_protocol_v2_fetch_sideband_all_response(&mut encoded, &sections)
13697            .expect("test operation should succeed");
13698        encoded.extend_from_slice(b"tail");
13699
13700        let mut input = encoded.as_slice();
13701        assert_eq!(
13702            read_protocol_v2_fetch_sideband_all_response(ObjectFormat::Sha1, &mut input)
13703                .expect("test operation should succeed"),
13704            ProtocolV2FetchSidebandAllResponse {
13705                sections: sections.clone(),
13706                progress: Vec::new(),
13707            }
13708        );
13709        assert_eq!(input, b"tail");
13710
13711        let mut encoded = Vec::new();
13712        write_protocol_v2_fetch_sideband_all_response_with_response_end(&mut encoded, &sections)
13713            .expect("test operation should succeed");
13714        encoded.extend_from_slice(b"tail");
13715
13716        let mut input = encoded.as_slice();
13717        assert_eq!(
13718            read_protocol_v2_fetch_sideband_all_response_until_response_end(
13719                ObjectFormat::Sha1,
13720                &mut input,
13721            )
13722            .expect("test operation should succeed")
13723            .sections,
13724            sections
13725        );
13726        assert_eq!(input, b"tail");
13727    }
13728
13729    #[test]
13730    fn protocol_v2_fetch_sideband_all_response_rejects_malformed_sideband() {
13731        assert!(
13732            parse_protocol_v2_fetch_sideband_all_response(
13733                ObjectFormat::Sha1,
13734                &[
13735                    PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13736                    PktLineFrame::Flush,
13737                ],
13738            )
13739            .is_err()
13740        );
13741        assert!(
13742            parse_protocol_v2_fetch_sideband_all_response(
13743                ObjectFormat::Sha1,
13744                &[
13745                    PktLineFrame::Data(
13746                        encode_sideband_packet(&SideBandPacket {
13747                            channel: SideBandChannel::Fatal,
13748                            data: b"remote died\n".to_vec(),
13749                        })
13750                        .expect("test operation should succeed"),
13751                    ),
13752                    PktLineFrame::Flush,
13753                ],
13754            )
13755            .is_err()
13756        );
13757    }
13758
13759    #[test]
13760    fn protocol_v2_object_info_response_parses_and_encodes_size_records() {
13761        let oid = ObjectId::from_hex(
13762            ObjectFormat::Sha1,
13763            "1111111111111111111111111111111111111111",
13764        )
13765        .expect("test operation should succeed");
13766        let frames = vec![
13767            PktLineFrame::Data(b"size\n".to_vec()),
13768            PktLineFrame::Data(b"1111111111111111111111111111111111111111 12345\n".to_vec()),
13769            PktLineFrame::Flush,
13770        ];
13771        let response = parse_protocol_v2_object_info_response(ObjectFormat::Sha1, &frames)
13772            .expect("test operation should succeed");
13773        assert_eq!(
13774            response,
13775            ProtocolV2ObjectInfoResponse {
13776                size: true,
13777                records: vec![ProtocolV2ObjectInfoRecord { oid, size: 12345 }],
13778            }
13779        );
13780        assert_eq!(
13781            encode_protocol_v2_object_info_response(&response)
13782                .expect("test operation should succeed"),
13783            frames
13784        );
13785    }
13786
13787    #[test]
13788    fn protocol_v2_object_info_response_streams_and_exchanges() {
13789        let request = ProtocolV2ObjectInfoRequest {
13790            size: true,
13791            oids: vec![
13792                ObjectId::from_hex(
13793                    ObjectFormat::Sha1,
13794                    "1111111111111111111111111111111111111111",
13795                )
13796                .expect("test operation should succeed"),
13797            ],
13798        };
13799        let response = ProtocolV2ObjectInfoResponse {
13800            size: true,
13801            records: vec![ProtocolV2ObjectInfoRecord {
13802                oid: request.oids[0].clone(),
13803                size: 7,
13804            }],
13805        };
13806
13807        let mut encoded = Vec::new();
13808        write_protocol_v2_object_info_response(&mut encoded, &response)
13809            .expect("test operation should succeed");
13810        encoded.extend_from_slice(b"tail");
13811        let mut input = encoded.as_slice();
13812        assert_eq!(
13813            read_protocol_v2_object_info_response(ObjectFormat::Sha1, &mut input)
13814                .expect("test operation should succeed"),
13815            response
13816        );
13817        assert_eq!(input, b"tail");
13818
13819        let mut response_bytes = Vec::new();
13820        write_protocol_v2_object_info_response(&mut response_bytes, &response)
13821            .expect("test operation should succeed");
13822        let mut input = response_bytes.as_slice();
13823        let mut output = Vec::new();
13824        assert_eq!(
13825            exchange_protocol_v2_object_info(
13826                ObjectFormat::Sha1,
13827                &mut input,
13828                &mut output,
13829                &request,
13830            )
13831            .expect("test operation should succeed"),
13832            response
13833        );
13834        assert!(input.is_empty());
13835        let mut output_read = output.as_slice();
13836        assert_eq!(
13837            read_protocol_v2_object_info_request(ObjectFormat::Sha1, &mut output_read)
13838                .expect("test operation should succeed"),
13839            request
13840        );
13841    }
13842
13843    #[test]
13844    fn protocol_v2_object_info_response_rejects_malformed_records() {
13845        assert!(parse_protocol_v2_object_info_response(ObjectFormat::Sha1, &[]).is_err());
13846        assert!(
13847            parse_protocol_v2_object_info_response(
13848                ObjectFormat::Sha1,
13849                &[PktLineFrame::Data(b"size\n".to_vec())],
13850            )
13851            .is_err()
13852        );
13853        assert!(
13854            parse_protocol_v2_object_info_response(
13855                ObjectFormat::Sha1,
13856                &[PktLineFrame::Data(b"type\n".to_vec()), PktLineFrame::Flush,],
13857            )
13858            .is_err()
13859        );
13860        assert!(
13861            parse_protocol_v2_object_info_response(
13862                ObjectFormat::Sha1,
13863                &[
13864                    PktLineFrame::Data(b"size\n".to_vec()),
13865                    PktLineFrame::Data(
13866                        b"1111111111111111111111111111111111111111 not-a-size\n".to_vec()
13867                    ),
13868                    PktLineFrame::Flush,
13869                ],
13870            )
13871            .is_err()
13872        );
13873        assert!(
13874            parse_protocol_v2_object_info_response(
13875                ObjectFormat::Sha1,
13876                &[
13877                    PktLineFrame::Data(b"size\n".to_vec()),
13878                    PktLineFrame::Delimiter,
13879                    PktLineFrame::Flush,
13880                ],
13881            )
13882            .is_err()
13883        );
13884        assert!(
13885            encode_protocol_v2_object_info_response(&ProtocolV2ObjectInfoResponse {
13886                size: false,
13887                records: Vec::new(),
13888            })
13889            .is_err()
13890        );
13891    }
13892
13893    #[test]
13894    fn protocol_v2_fetch_response_reads_stateless_response_end() {
13895        let sections = vec![ProtocolV2FetchResponseSection::Acknowledgments(vec![
13896            ProtocolV2FetchAcknowledgment::Nak,
13897        ])];
13898        let mut encoded = Vec::new();
13899        write_protocol_v2_fetch_response_with_response_end(&mut encoded, &sections)
13900            .expect("test operation should succeed");
13901        encoded.extend_from_slice(b"tail");
13902
13903        let mut input = encoded.as_slice();
13904        assert_eq!(
13905            read_protocol_v2_fetch_response_until_response_end(ObjectFormat::Sha1, &mut input)
13906                .expect("test operation should succeed"),
13907            sections
13908        );
13909        assert_eq!(input, b"tail");
13910        assert!(
13911            parse_protocol_v2_fetch_response(
13912                ObjectFormat::Sha1,
13913                &[
13914                    PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13915                    PktLineFrame::ResponseEnd,
13916                ],
13917            )
13918            .is_err()
13919        );
13920    }
13921
13922    #[test]
13923    fn protocol_v2_fetch_exchange_writes_request_and_reads_response() {
13924        let want = ObjectId::from_hex(
13925            ObjectFormat::Sha1,
13926            "1111111111111111111111111111111111111111",
13927        )
13928        .expect("test operation should succeed");
13929        let request = ProtocolV2FetchRequest {
13930            wants: vec![want],
13931            thin_pack: true,
13932            done: true,
13933            ..ProtocolV2FetchRequest::default()
13934        };
13935        let sections = vec![ProtocolV2FetchResponseSection::Acknowledgments(vec![
13936            ProtocolV2FetchAcknowledgment::Nak,
13937        ])];
13938        let mut response = Vec::new();
13939        write_protocol_v2_fetch_response(&mut response, &sections)
13940            .expect("test operation should succeed");
13941
13942        let mut input = response.as_slice();
13943        let mut output = Vec::new();
13944        assert_eq!(
13945            exchange_protocol_v2_fetch(ObjectFormat::Sha1, &mut input, &mut output, &request)
13946                .expect("test operation should succeed"),
13947            sections
13948        );
13949        assert!(input.is_empty());
13950        let mut output_read = output.as_slice();
13951        assert_eq!(
13952            read_protocol_v2_fetch_request(ObjectFormat::Sha1, &mut output_read)
13953                .expect("test operation should succeed"),
13954            request
13955        );
13956    }
13957
13958    #[test]
13959    fn protocol_v2_fetch_packfile_demuxes_sideband_section() {
13960        let sections = vec![
13961            ProtocolV2FetchResponseSection::Acknowledgments(vec![
13962                ProtocolV2FetchAcknowledgment::Nak,
13963            ]),
13964            ProtocolV2FetchResponseSection::Packfile(vec![
13965                b"\x01PACK".to_vec(),
13966                b"\x02counting objects\n".to_vec(),
13967                b"\x01 bytes".to_vec(),
13968                b"\x02done\n".to_vec(),
13969            ]),
13970        ];
13971
13972        assert_eq!(
13973            demux_protocol_v2_fetch_packfile(&sections).expect("test operation should succeed"),
13974            Some(SideBandDemux {
13975                data: b"PACK bytes".to_vec(),
13976                progress: vec![b"counting objects\n".to_vec(), b"done\n".to_vec()],
13977            })
13978        );
13979        assert_eq!(
13980            demux_protocol_v2_fetch_packfile(&[ProtocolV2FetchResponseSection::Acknowledgments(
13981                vec![ProtocolV2FetchAcknowledgment::Nak],
13982            )])
13983            .expect("test operation should succeed"),
13984            None
13985        );
13986    }
13987
13988    #[test]
13989    fn protocol_v2_fetch_packfile_demux_rejects_duplicate_or_bad_sideband() {
13990        assert!(
13991            demux_protocol_v2_fetch_packfile(&[
13992                ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK".to_vec()]),
13993                ProtocolV2FetchResponseSection::Packfile(vec![b"\x01more".to_vec()]),
13994            ])
13995            .is_err()
13996        );
13997        assert!(
13998            demux_protocol_v2_fetch_packfile(&[ProtocolV2FetchResponseSection::Packfile(vec![
13999                b"\x03remote died\n".to_vec()
14000            ])])
14001            .is_err()
14002        );
14003        assert!(
14004            demux_protocol_v2_fetch_packfile(&[ProtocolV2FetchResponseSection::Packfile(vec![
14005                b"\x04bad".to_vec()
14006            ])])
14007            .is_err()
14008        );
14009    }
14010
14011    #[test]
14012    fn protocol_v2_fetch_response_rejects_malformed_sections() {
14013        assert!(
14014            parse_protocol_v2_fetch_response(
14015                ObjectFormat::Sha1,
14016                &[PktLineFrame::Data(b"acknowledgments\n".to_vec())],
14017            )
14018            .is_err()
14019        );
14020        assert!(
14021            parse_protocol_v2_fetch_response(
14022                ObjectFormat::Sha1,
14023                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
14024            )
14025            .is_err()
14026        );
14027        assert!(
14028            parse_protocol_v2_fetch_response(
14029                ObjectFormat::Sha1,
14030                &[
14031                    PktLineFrame::Data(b"acknowledgments\n".to_vec()),
14032                    PktLineFrame::Data(b"ACK not-an-oid\n".to_vec()),
14033                    PktLineFrame::Flush,
14034                ],
14035            )
14036            .is_err()
14037        );
14038        assert!(
14039            parse_protocol_v2_fetch_response(
14040                ObjectFormat::Sha1,
14041                &[
14042                    PktLineFrame::Data(b"packfile-uris\n".to_vec()),
14043                    PktLineFrame::Data(b"https://example.invalid/pack-a.pack\n".to_vec()),
14044                    PktLineFrame::Flush,
14045                ],
14046            )
14047            .is_err()
14048        );
14049        assert!(
14050            parse_protocol_v2_fetch_response(
14051                ObjectFormat::Sha1,
14052                &[
14053                    PktLineFrame::Data(b"packfile-uris\n".to_vec()),
14054                    PktLineFrame::Data(
14055                        b"not-a-hash https://example.invalid/pack-a.pack\n".to_vec()
14056                    ),
14057                    PktLineFrame::Flush,
14058                ],
14059            )
14060            .is_err()
14061        );
14062        assert!(
14063            encode_protocol_v2_fetch_response(&[ProtocolV2FetchResponseSection::WantedRefs(vec![
14064                ProtocolV2FetchWantedRef {
14065                    oid: ObjectId::from_hex(
14066                        ObjectFormat::Sha1,
14067                        "1111111111111111111111111111111111111111",
14068                    )
14069                    .expect("test operation should succeed"),
14070                    name: "bad ref".into(),
14071                }
14072            ])])
14073            .is_err()
14074        );
14075    }
14076
14077    #[test]
14078    fn protocol_v2_ls_refs_response_bridges_into_ref_advertisement_set() {
14079        let head = ObjectId::from_hex(
14080            ObjectFormat::Sha1,
14081            "1111111111111111111111111111111111111111",
14082        )
14083        .expect("test operation should succeed");
14084        let tag = ObjectId::from_hex(
14085            ObjectFormat::Sha1,
14086            "2222222222222222222222222222222222222222",
14087        )
14088        .expect("test operation should succeed");
14089        let tag_peeled = ObjectId::from_hex(
14090            ObjectFormat::Sha1,
14091            "3333333333333333333333333333333333333333",
14092        )
14093        .expect("test operation should succeed");
14094        let frames = vec![
14095            PktLineFrame::Data(
14096                b"1111111111111111111111111111111111111111 HEAD symref-target:refs/heads/main\n"
14097                    .to_vec(),
14098            ),
14099            PktLineFrame::Data(
14100                b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec(),
14101            ),
14102            PktLineFrame::Data(
14103                b"2222222222222222222222222222222222222222 refs/tags/v1 peeled:3333333333333333333333333333333333333333\n"
14104                    .to_vec(),
14105            ),
14106            PktLineFrame::Flush,
14107        ];
14108
14109        let set = parse_protocol_v2_ls_refs_response_as_ref_advertisement_set(
14110            ObjectFormat::Sha1,
14111            &frames,
14112        )
14113        .expect("test operation should succeed");
14114        assert_eq!(
14115            set,
14116            RefAdvertisementSet {
14117                protocol: ProtocolVersion::V2,
14118                refs: vec![
14119                    RefAdvertisement {
14120                        oid: head.clone(),
14121                        name: "HEAD".into(),
14122                        capabilities: vec![Capability {
14123                            name: "symref".into(),
14124                            value: Some("HEAD:refs/heads/main".into()),
14125                        }],
14126                    },
14127                    RefAdvertisement {
14128                        oid: head,
14129                        name: "refs/heads/main".into(),
14130                        capabilities: Vec::new(),
14131                    },
14132                    RefAdvertisement {
14133                        oid: tag,
14134                        name: "refs/tags/v1".into(),
14135                        capabilities: Vec::new(),
14136                    },
14137                    RefAdvertisement {
14138                        oid: tag_peeled,
14139                        name: "refs/tags/v1^{}".into(),
14140                        capabilities: Vec::new(),
14141                    },
14142                ],
14143                shallow: Vec::new(),
14144            }
14145        );
14146
14147        // The streaming reader path produces the same bridged set.
14148        let mut encoded = Vec::new();
14149        write_pkt_line_frames(&mut encoded, &frames).expect("test operation should succeed");
14150        encoded.extend_from_slice(b"tail");
14151        let mut input = encoded.as_slice();
14152        assert_eq!(
14153            read_protocol_v2_ls_refs_response_as_ref_advertisement_set(
14154                ObjectFormat::Sha1,
14155                &mut input,
14156            )
14157            .expect("test operation should succeed"),
14158            set,
14159        );
14160        assert_eq!(input, b"tail");
14161    }
14162
14163    #[test]
14164    fn protocol_v2_ls_refs_records_bridge_unborn_head_symref_and_empty() {
14165        // An unborn HEAD pointing at an as-yet-uncreated branch carries only a
14166        // symref capability and has no concrete ref to attach it to.
14167        let records = vec![ProtocolV2LsRefsRecord::Unborn {
14168            name: "HEAD".into(),
14169            symref_target: Some("refs/heads/main".into()),
14170            attributes: Vec::new(),
14171        }];
14172        assert!(protocol_v2_ls_refs_records_to_ref_advertisement_set(&records).is_err());
14173
14174        // An empty ls-refs response bridges to an empty v2 set.
14175        assert_eq!(
14176            protocol_v2_ls_refs_records_to_ref_advertisement_set(&[])
14177                .expect("test operation should succeed"),
14178            RefAdvertisementSet {
14179                protocol: ProtocolVersion::V2,
14180                refs: Vec::new(),
14181                shallow: Vec::new(),
14182            }
14183        );
14184
14185        // An unborn HEAD alongside a concrete ref attaches the symref to the
14186        // first ref, matching the v0/v1 advertisement convention.
14187        let main = ObjectId::from_hex(
14188            ObjectFormat::Sha1,
14189            "4444444444444444444444444444444444444444",
14190        )
14191        .expect("test operation should succeed");
14192        let records = vec![
14193            ProtocolV2LsRefsRecord::Unborn {
14194                name: "HEAD".into(),
14195                symref_target: Some("refs/heads/main".into()),
14196                attributes: Vec::new(),
14197            },
14198            ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
14199                oid: main.clone(),
14200                name: "refs/heads/main".into(),
14201                peeled: None,
14202                symref_target: None,
14203                attributes: Vec::new(),
14204            }),
14205        ];
14206        let set = protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
14207            .expect("test operation should succeed");
14208        assert_eq!(
14209            set,
14210            RefAdvertisementSet {
14211                protocol: ProtocolVersion::V2,
14212                refs: vec![RefAdvertisement {
14213                    oid: main,
14214                    name: "refs/heads/main".into(),
14215                    capabilities: vec![Capability {
14216                        name: "symref".into(),
14217                        value: Some("HEAD:refs/heads/main".into()),
14218                    }],
14219                }],
14220                shallow: Vec::new(),
14221            }
14222        );
14223    }
14224}