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 mut capabilities = Vec::new();
1001    if options.report_status_v2 {
1002        require_receive_pack_feature(features.report_status_v2, "report-status-v2")?;
1003        capabilities.push(Capability {
1004            name: "report-status-v2".into(),
1005            value: None,
1006        });
1007    } else if options.report_status {
1008        require_receive_pack_feature(features.report_status, "report-status")?;
1009        capabilities.push(Capability {
1010            name: "report-status".into(),
1011            value: None,
1012        });
1013    }
1014    if commands.iter().any(is_receive_pack_delete_command) {
1015        require_receive_pack_feature(features.delete_refs, "delete-refs")?;
1016        capabilities.push(Capability {
1017            name: "delete-refs".into(),
1018            value: None,
1019        });
1020    }
1021    if options.atomic {
1022        require_receive_pack_feature(features.atomic, "atomic")?;
1023        capabilities.push(Capability {
1024            name: "atomic".into(),
1025            value: None,
1026        });
1027    }
1028    if options.ofs_delta {
1029        require_receive_pack_feature(features.ofs_delta, "ofs-delta")?;
1030        capabilities.push(Capability {
1031            name: "ofs-delta".into(),
1032            value: None,
1033        });
1034    }
1035    if options.side_band_64k {
1036        require_receive_pack_feature(features.side_band_64k, "side-band-64k")?;
1037        capabilities.push(Capability {
1038            name: "side-band-64k".into(),
1039            value: None,
1040        });
1041    }
1042    if options.quiet {
1043        require_receive_pack_feature(features.quiet, "quiet")?;
1044        capabilities.push(Capability {
1045            name: "quiet".into(),
1046            value: None,
1047        });
1048    }
1049    if let Some(agent) = &options.agent {
1050        validate_capability_field("receive-pack request agent", agent)?;
1051        capabilities.push(Capability {
1052            name: "agent".into(),
1053            value: Some(agent.clone()),
1054        });
1055    }
1056    if let Some(format) = options.object_format {
1057        if features.object_format != Some(format) {
1058            return Err(GitError::InvalidFormat(
1059                "receive-pack request object-format was not advertised".into(),
1060            ));
1061        }
1062        capabilities.push(Capability {
1063            name: "object-format".into(),
1064            value: Some(format.name().into()),
1065        });
1066    }
1067    let push_options = if options.push_options.is_empty() {
1068        None
1069    } else {
1070        require_receive_pack_feature(features.push_options, "push-options")?;
1071        for option in &options.push_options {
1072            validate_receive_pack_push_option(option.as_bytes())?;
1073        }
1074        capabilities.push(Capability {
1075            name: "push-options".into(),
1076            value: None,
1077        });
1078        Some(options.push_options)
1079    };
1080    let request = ReceivePackPushRequest {
1081        commands: ReceivePackRequest {
1082            commands,
1083            capabilities,
1084            shallow: Vec::new(),
1085        },
1086        push_options,
1087        packfile,
1088    };
1089    validate_receive_pack_push_request_features(features, &request)?;
1090    Ok(request)
1091}
1092
1093pub fn smart_http_info_refs_path(repository_path: &str, service: GitService) -> Result<String> {
1094    validate_smart_http_service(service)?;
1095    let repository_path = normalize_http_repository_path(repository_path)?;
1096    Ok(format!(
1097        "{repository_path}/info/refs?service={}",
1098        service.as_str()
1099    ))
1100}
1101
1102pub fn smart_http_rpc_path(repository_path: &str, service: GitService) -> Result<String> {
1103    validate_smart_http_service(service)?;
1104    let repository_path = normalize_http_repository_path(repository_path)?;
1105    Ok(format!("{repository_path}/{}", service.as_str()))
1106}
1107
1108pub fn dumb_http_info_refs_path(repository_path: &str) -> Result<String> {
1109    let repository_path = normalize_http_repository_path(repository_path)?;
1110    Ok(format!("{repository_path}/info/refs"))
1111}
1112
1113pub fn dumb_http_alternates_path(repository_path: &str) -> Result<String> {
1114    let repository_path = normalize_http_repository_path(repository_path)?;
1115    Ok(format!("{repository_path}/objects/info/http-alternates"))
1116}
1117
1118pub fn dumb_http_packs_path(repository_path: &str) -> Result<String> {
1119    let repository_path = normalize_http_repository_path(repository_path)?;
1120    Ok(format!("{repository_path}/objects/info/packs"))
1121}
1122
1123pub fn dumb_http_loose_object_path(repository_path: &str, oid: &ObjectId) -> Result<String> {
1124    let repository_path = normalize_http_repository_path(repository_path)?;
1125    let oid = oid.to_string();
1126    let (directory, file) = oid.split_at(2);
1127    Ok(format!("{repository_path}/objects/{directory}/{file}"))
1128}
1129
1130pub fn dumb_http_pack_file_path(repository_path: &str, hash: &ObjectId) -> Result<String> {
1131    dumb_http_pack_resource_path(repository_path, hash, "pack")
1132}
1133
1134pub fn dumb_http_pack_index_path(repository_path: &str, hash: &ObjectId) -> Result<String> {
1135    dumb_http_pack_resource_path(repository_path, hash, "idx")
1136}
1137
1138pub fn smart_http_advertisement_content_type(service: GitService) -> Result<String> {
1139    validate_smart_http_service(service)?;
1140    Ok(format!("application/x-{}-advertisement", service.as_str()))
1141}
1142
1143pub fn smart_http_rpc_request_content_type(service: GitService) -> Result<String> {
1144    validate_smart_http_service(service)?;
1145    Ok(format!("application/x-{}-request", service.as_str()))
1146}
1147
1148pub fn smart_http_rpc_result_content_type(service: GitService) -> Result<String> {
1149    validate_smart_http_service(service)?;
1150    Ok(format!("application/x-{}-result", service.as_str()))
1151}
1152
1153pub fn parse_smart_http_advertisement_content_type(value: &str) -> Result<GitService> {
1154    parse_smart_http_content_type(value, "-advertisement")
1155}
1156
1157pub fn parse_smart_http_rpc_request_content_type(value: &str) -> Result<GitService> {
1158    parse_smart_http_content_type(value, "-request")
1159}
1160
1161pub fn parse_smart_http_rpc_result_content_type(value: &str) -> Result<GitService> {
1162    parse_smart_http_content_type(value, "-result")
1163}
1164
1165pub fn parse_sideband_packet(payload: &[u8]) -> Result<SideBandPacket> {
1166    let Some((&channel, data)) = payload.split_first() else {
1167        return Err(GitError::InvalidFormat("sideband packet is empty".into()));
1168    };
1169    let channel = match channel {
1170        1 => SideBandChannel::Data,
1171        2 => SideBandChannel::Progress,
1172        3 => SideBandChannel::Fatal,
1173        other => {
1174            return Err(GitError::InvalidFormat(format!(
1175                "invalid sideband channel {other}"
1176            )));
1177        }
1178    };
1179    Ok(SideBandPacket {
1180        channel,
1181        data: data.to_vec(),
1182    })
1183}
1184
1185pub fn encode_sideband_packet(packet: &SideBandPacket) -> Result<Vec<u8>> {
1186    let mut out = Vec::with_capacity(packet.data.len() + 1);
1187    out.push(match packet.channel {
1188        SideBandChannel::Data => 1,
1189        SideBandChannel::Progress => 2,
1190        SideBandChannel::Fatal => 3,
1191    });
1192    out.extend_from_slice(&packet.data);
1193    if out.len() > PKT_LINE_MAX_PAYLOAD_LEN {
1194        return Err(GitError::InvalidFormat(format!(
1195            "sideband packet exceeds {PKT_LINE_MAX_PAYLOAD_LEN} bytes"
1196        )));
1197    }
1198    Ok(out)
1199}
1200
1201pub fn write_sideband_packet(writer: &mut impl Write, packet: &SideBandPacket) -> Result<()> {
1202    write_sideband_payload(writer, packet.channel, &packet.data)
1203}
1204
1205fn write_sideband_payload(
1206    writer: &mut impl Write,
1207    channel: SideBandChannel,
1208    data: &[u8],
1209) -> Result<()> {
1210    let payload_len = data
1211        .len()
1212        .checked_add(1)
1213        .ok_or_else(|| GitError::InvalidFormat("sideband packet length overflow".into()))?;
1214    if payload_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    writer.write_all(&pkt_line_header(payload_len + 4))?;
1220    writer.write_all(&[match channel {
1221        SideBandChannel::Data => 1,
1222        SideBandChannel::Progress => 2,
1223        SideBandChannel::Fatal => 3,
1224    }])?;
1225    writer.write_all(data)?;
1226    Ok(())
1227}
1228
1229pub fn parse_sideband_packets(payloads: &[Vec<u8>]) -> Result<Vec<SideBandPacket>> {
1230    payloads
1231        .iter()
1232        .map(|payload| parse_sideband_packet(payload))
1233        .collect()
1234}
1235
1236pub fn encode_sideband_packets(packets: &[SideBandPacket]) -> Result<Vec<Vec<u8>>> {
1237    packets.iter().map(encode_sideband_packet).collect()
1238}
1239
1240pub fn parse_sideband_stream(frames: &[PktLineFrame]) -> Result<Vec<SideBandPacket>> {
1241    let mut packets = Vec::new();
1242    let mut saw_flush = false;
1243    for (idx, frame) in frames.iter().enumerate() {
1244        match frame {
1245            PktLineFrame::Data(payload) if !saw_flush => {
1246                packets.push(parse_sideband_packet(payload)?);
1247            }
1248            PktLineFrame::Data(_) => {
1249                return Err(GitError::InvalidFormat(
1250                    "sideband stream has data after flush".into(),
1251                ));
1252            }
1253            PktLineFrame::Flush => {
1254                saw_flush = true;
1255                if idx + 1 != frames.len() {
1256                    return Err(GitError::InvalidFormat(
1257                        "sideband stream has frames after flush".into(),
1258                    ));
1259                }
1260            }
1261            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
1262                return Err(GitError::InvalidFormat(
1263                    "sideband stream contains a non-flush control packet".into(),
1264                ));
1265            }
1266        }
1267    }
1268    if !saw_flush {
1269        return Err(GitError::InvalidFormat(
1270            "sideband stream missing flush".into(),
1271        ));
1272    }
1273    Ok(packets)
1274}
1275
1276pub fn encode_sideband_stream(packets: &[SideBandPacket]) -> Result<Vec<PktLineFrame>> {
1277    let mut frames = Vec::new();
1278    for packet in packets {
1279        frames.push(PktLineFrame::data(encode_sideband_packet(packet)?)?);
1280    }
1281    frames.push(PktLineFrame::Flush);
1282    Ok(frames)
1283}
1284
1285pub fn read_sideband_stream(reader: &mut impl Read) -> Result<Vec<SideBandPacket>> {
1286    let frames = read_pkt_line_frames_until_flush(reader)?;
1287    parse_sideband_stream(&frames)
1288}
1289
1290pub fn write_sideband_stream(writer: &mut impl Write, packets: &[SideBandPacket]) -> Result<()> {
1291    for packet in packets {
1292        write_sideband_packet(writer, packet)?;
1293    }
1294    writer.write_all(b"0000")?;
1295    Ok(())
1296}
1297
1298pub fn demux_sideband_packets(packets: &[SideBandPacket]) -> Result<SideBandDemux> {
1299    let mut out = SideBandDemux::default();
1300    for packet in packets {
1301        match packet.channel {
1302            SideBandChannel::Data => out.data.extend_from_slice(&packet.data),
1303            SideBandChannel::Progress => out.progress.push(packet.data.clone()),
1304            SideBandChannel::Fatal => {
1305                let message = String::from_utf8_lossy(&packet.data).into_owned();
1306                return Err(GitError::InvalidFormat(format!(
1307                    "sideband fatal: {message}"
1308                )));
1309            }
1310        }
1311    }
1312    Ok(out)
1313}
1314
1315pub fn parse_and_demux_sideband_packets(payloads: &[Vec<u8>]) -> Result<SideBandDemux> {
1316    let packets = parse_sideband_packets(payloads)?;
1317    demux_sideband_packets(&packets)
1318}
1319
1320pub fn demux_sideband_stream(frames: &[PktLineFrame]) -> Result<SideBandDemux> {
1321    let packets = parse_sideband_stream(frames)?;
1322    demux_sideband_packets(&packets)
1323}
1324
1325pub fn read_and_demux_sideband_stream(reader: &mut impl Read) -> Result<SideBandDemux> {
1326    let packets = read_sideband_stream(reader)?;
1327    demux_sideband_packets(&packets)
1328}
1329
1330pub fn parse_upload_archive_request(frames: &[PktLineFrame]) -> Result<UploadArchiveRequest> {
1331    let mut request = UploadArchiveRequest::default();
1332    let mut saw_flush = false;
1333    for (idx, frame) in frames.iter().enumerate() {
1334        match frame {
1335            PktLineFrame::Data(payload) if !saw_flush => {
1336                let text = parse_protocol_v2_line_text("upload-archive request argument", payload)?;
1337                let argument = text.strip_prefix("argument ").ok_or_else(|| {
1338                    GitError::InvalidFormat("upload-archive request line must be argument".into())
1339                })?;
1340                validate_upload_archive_argument(argument)?;
1341                request.arguments.push(argument.to_string());
1342            }
1343            PktLineFrame::Data(_) => {
1344                return Err(GitError::InvalidFormat(
1345                    "upload-archive request has data after flush".into(),
1346                ));
1347            }
1348            PktLineFrame::Flush => {
1349                saw_flush = true;
1350                if idx + 1 != frames.len() {
1351                    return Err(GitError::InvalidFormat(
1352                        "upload-archive request has frames after flush".into(),
1353                    ));
1354                }
1355            }
1356            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
1357                return Err(GitError::InvalidFormat(
1358                    "upload-archive request contains a non-flush control packet".into(),
1359                ));
1360            }
1361        }
1362    }
1363    if !saw_flush {
1364        return Err(GitError::InvalidFormat(
1365            "upload-archive request missing flush".into(),
1366        ));
1367    }
1368    if request.arguments.is_empty() {
1369        return Err(GitError::InvalidFormat(
1370            "upload-archive request is missing arguments".into(),
1371        ));
1372    }
1373    Ok(request)
1374}
1375
1376pub fn encode_upload_archive_request(request: &UploadArchiveRequest) -> Result<Vec<PktLineFrame>> {
1377    if request.arguments.is_empty() {
1378        return Err(GitError::InvalidFormat(
1379            "upload-archive request is missing arguments".into(),
1380        ));
1381    }
1382    let mut frames = Vec::new();
1383    for argument in &request.arguments {
1384        validate_upload_archive_argument(argument)?;
1385        frames.push(PktLineFrame::data(line_from_str(&format!(
1386            "argument {argument}"
1387        )))?);
1388    }
1389    frames.push(PktLineFrame::Flush);
1390    Ok(frames)
1391}
1392
1393pub fn read_upload_archive_request(reader: &mut impl Read) -> Result<UploadArchiveRequest> {
1394    let frames = read_pkt_line_frames_until_flush(reader)?;
1395    parse_upload_archive_request(&frames)
1396}
1397
1398pub fn write_upload_archive_request(
1399    writer: &mut impl Write,
1400    request: &UploadArchiveRequest,
1401) -> Result<()> {
1402    if request.arguments.is_empty() {
1403        return Err(GitError::InvalidFormat(
1404            "upload-archive request is missing arguments".into(),
1405        ));
1406    }
1407    for argument in &request.arguments {
1408        validate_upload_archive_argument(argument)?;
1409        write_pkt_line_payload(writer, &line_from_str(&format!("argument {argument}")))?;
1410    }
1411    writer.write_all(b"0000")?;
1412    Ok(())
1413}
1414
1415pub fn parse_upload_archive_response(frames: &[PktLineFrame]) -> Result<UploadArchiveResponse> {
1416    let Some((first, rest)) = frames.split_first() else {
1417        return Err(GitError::InvalidFormat(
1418            "upload-archive response is empty".into(),
1419        ));
1420    };
1421    let PktLineFrame::Data(payload) = first else {
1422        return Err(GitError::InvalidFormat(
1423            "upload-archive response must start with a data packet".into(),
1424        ));
1425    };
1426    let text = parse_protocol_v2_line_text("upload-archive response status", payload)?;
1427    if text == "ACK" {
1428        return Ok(UploadArchiveResponse::Ack {
1429            sideband: parse_sideband_stream(rest)?,
1430        });
1431    }
1432    if let Some(message) = text.strip_prefix("NACK ") {
1433        validate_upload_archive_status_message(message)?;
1434        if !matches!(rest, [PktLineFrame::Flush]) {
1435            return Err(GitError::InvalidFormat(
1436                "upload-archive NACK response must end with flush".into(),
1437            ));
1438        }
1439        return Ok(UploadArchiveResponse::Nack {
1440            message: message.to_string(),
1441        });
1442    }
1443    Err(GitError::InvalidFormat(format!(
1444        "unsupported upload-archive response status {text}"
1445    )))
1446}
1447
1448pub fn encode_upload_archive_response(
1449    response: &UploadArchiveResponse,
1450) -> Result<Vec<PktLineFrame>> {
1451    let mut frames = Vec::new();
1452    match response {
1453        UploadArchiveResponse::Ack { sideband } => {
1454            frames.push(PktLineFrame::data(line_from_str("ACK"))?);
1455            frames.extend(encode_sideband_stream(sideband)?);
1456        }
1457        UploadArchiveResponse::Nack { message } => {
1458            validate_upload_archive_status_message(message)?;
1459            frames.push(PktLineFrame::data(line_from_str(&format!(
1460                "NACK {message}"
1461            )))?);
1462            frames.push(PktLineFrame::Flush);
1463        }
1464    }
1465    Ok(frames)
1466}
1467
1468pub fn read_upload_archive_response(reader: &mut impl Read) -> Result<UploadArchiveResponse> {
1469    let frames = read_pkt_line_frames_until_flush(reader)?;
1470    parse_upload_archive_response(&frames)
1471}
1472
1473pub fn write_upload_archive_response(
1474    writer: &mut impl Write,
1475    response: &UploadArchiveResponse,
1476) -> Result<()> {
1477    match response {
1478        UploadArchiveResponse::Ack { sideband } => {
1479            write_pkt_line_payload(writer, b"ACK\n")?;
1480            write_sideband_stream(writer, sideband)?;
1481        }
1482        UploadArchiveResponse::Nack { message } => {
1483            validate_upload_archive_status_message(message)?;
1484            write_pkt_line_payload(writer, &line_from_str(&format!("NACK {message}")))?;
1485            writer.write_all(b"0000")?;
1486        }
1487    }
1488    Ok(())
1489}
1490
1491pub fn demux_upload_archive_response(response: &UploadArchiveResponse) -> Result<SideBandDemux> {
1492    match response {
1493        UploadArchiveResponse::Ack { sideband } => demux_sideband_packets(sideband),
1494        UploadArchiveResponse::Nack { message } => Err(GitError::InvalidFormat(format!(
1495            "upload-archive NACK: {message}"
1496        ))),
1497    }
1498}
1499
1500fn parse_pkt_len(bytes: &[u8]) -> Result<usize> {
1501    let mut len = 0usize;
1502    for byte in bytes {
1503        len = (len << 4) | hex_nibble(*byte)? as usize;
1504    }
1505    Ok(len)
1506}
1507
1508fn hex_nibble(byte: u8) -> Result<u8> {
1509    match byte {
1510        b'0'..=b'9' => Ok(byte - b'0'),
1511        b'a'..=b'f' => Ok(byte - b'a' + 10),
1512        b'A'..=b'F' => Ok(byte - b'A' + 10),
1513        _ => Err(GitError::InvalidFormat(format!(
1514            "invalid pkt-line length byte {byte:#04x}"
1515        ))),
1516    }
1517}
1518
1519#[derive(Debug, Clone, PartialEq, Eq)]
1520pub struct TransportHandshake {
1521    pub protocol: ProtocolVersion,
1522    pub capabilities: Vec<Capability>,
1523}
1524
1525#[derive(Debug, Clone, PartialEq, Eq)]
1526pub struct RefAdvertisement {
1527    pub oid: ObjectId,
1528    pub name: String,
1529    pub capabilities: Vec<Capability>,
1530}
1531
1532#[derive(Debug, Clone, PartialEq, Eq)]
1533pub struct DumbHttpRefRecord {
1534    pub oid: ObjectId,
1535    pub name: String,
1536    pub peeled: bool,
1537}
1538
1539#[derive(Debug, Clone, PartialEq, Eq)]
1540pub struct DumbHttpPackRecord {
1541    pub hash: ObjectId,
1542}
1543
1544#[derive(Debug, Clone, PartialEq, Eq)]
1545pub struct RefAdvertisementSet {
1546    pub protocol: ProtocolVersion,
1547    pub refs: Vec<RefAdvertisement>,
1548    pub shallow: Vec<ObjectId>,
1549}
1550
1551#[derive(Debug, Clone, PartialEq, Eq, Default)]
1552pub struct UploadPackRequest {
1553    pub wants: Vec<ObjectId>,
1554    pub capabilities: Vec<Capability>,
1555    pub shallow: Vec<ObjectId>,
1556    pub deepen: Option<u32>,
1557    pub deepen_since: Option<u64>,
1558    pub deepen_not: Vec<String>,
1559    pub filter: Option<String>,
1560}
1561
1562#[derive(Debug, Clone, PartialEq, Eq, Default)]
1563pub struct UploadPackFeatures {
1564    pub multi_ack: bool,
1565    pub multi_ack_detailed: bool,
1566    pub no_done: bool,
1567    pub thin_pack: bool,
1568    pub side_band: bool,
1569    pub side_band_64k: bool,
1570    pub ofs_delta: bool,
1571    pub shallow: bool,
1572    pub deepen_since: bool,
1573    pub deepen_not: bool,
1574    pub include_tag: bool,
1575    pub no_progress: bool,
1576    pub allow_tip_sha1_in_want: bool,
1577    pub allow_reachable_sha1_in_want: bool,
1578    pub filter: bool,
1579    pub agent: Option<String>,
1580    pub object_format: Option<ObjectFormat>,
1581    pub symrefs: Vec<String>,
1582    pub unknown: Vec<Capability>,
1583}
1584
1585#[derive(Debug, Clone, PartialEq, Eq, Default)]
1586pub struct UploadPackNegotiationRequest {
1587    pub haves: Vec<ObjectId>,
1588    pub done: bool,
1589}
1590
1591#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1592pub enum UploadPackAckStatus {
1593    Continue,
1594    Common,
1595    Ready,
1596}
1597
1598#[derive(Debug, Clone, PartialEq, Eq)]
1599pub enum UploadPackAcknowledgment {
1600    Nak,
1601    Ack {
1602        oid: ObjectId,
1603        status: Option<UploadPackAckStatus>,
1604    },
1605}
1606
1607#[derive(Debug, Clone, PartialEq, Eq, Default)]
1608pub struct UploadPackPackfileResponse {
1609    pub acknowledgments: Vec<UploadPackAcknowledgment>,
1610    pub sideband: Vec<SideBandPacket>,
1611}
1612
1613#[derive(Debug, Clone, PartialEq, Eq, Default)]
1614pub struct UploadPackRawPackfileResponse {
1615    pub acknowledgments: Vec<UploadPackAcknowledgment>,
1616    pub packfile: Vec<u8>,
1617}
1618
1619#[derive(Debug, Clone, PartialEq, Eq)]
1620pub struct ReceivePackCommand {
1621    pub old_id: ObjectId,
1622    pub new_id: ObjectId,
1623    pub name: String,
1624}
1625
1626#[derive(Debug, Clone, PartialEq, Eq, Default)]
1627pub struct ReceivePackRequest {
1628    pub shallow: Vec<ObjectId>,
1629    pub commands: Vec<ReceivePackCommand>,
1630    pub capabilities: Vec<Capability>,
1631}
1632
1633#[derive(Debug, Clone, PartialEq, Eq, Default)]
1634pub struct ReceivePackPushRequest {
1635    pub commands: ReceivePackRequest,
1636    pub push_options: Option<Vec<String>>,
1637    pub packfile: Vec<u8>,
1638}
1639
1640#[derive(Debug, Clone, PartialEq, Eq, Default)]
1641pub struct ReceivePackPushRequestOptions {
1642    pub report_status: bool,
1643    pub report_status_v2: bool,
1644    pub atomic: bool,
1645    pub ofs_delta: bool,
1646    pub side_band_64k: bool,
1647    pub quiet: bool,
1648    pub agent: Option<String>,
1649    pub object_format: Option<ObjectFormat>,
1650    pub push_options: Vec<String>,
1651}
1652
1653#[derive(Debug, Clone, PartialEq, Eq, Default)]
1654pub struct ReceivePackFeatures {
1655    pub report_status: bool,
1656    pub report_status_v2: bool,
1657    pub delete_refs: bool,
1658    pub ofs_delta: bool,
1659    pub atomic: bool,
1660    pub push_options: bool,
1661    pub side_band_64k: bool,
1662    pub quiet: bool,
1663    pub no_thin: bool,
1664    pub agent: Option<String>,
1665    pub object_format: Option<ObjectFormat>,
1666    pub unknown: Vec<Capability>,
1667}
1668
1669#[derive(Debug, Clone, PartialEq, Eq)]
1670pub enum ReceivePackUnpackStatus {
1671    Ok,
1672    Error(String),
1673}
1674
1675#[derive(Debug, Clone, PartialEq, Eq)]
1676pub enum ReceivePackCommandStatus {
1677    Ok { name: String },
1678    Ng { name: String, message: String },
1679}
1680
1681#[derive(Debug, Clone, PartialEq, Eq)]
1682pub struct ReceivePackReportStatus {
1683    pub unpack: ReceivePackUnpackStatus,
1684    pub commands: Vec<ReceivePackCommandStatus>,
1685}
1686
1687#[derive(Debug, Clone, PartialEq, Eq, Default)]
1688pub struct ReceivePackCommandStatusV2Options {
1689    pub refname: Option<String>,
1690    pub old_oid: Option<ObjectId>,
1691    pub new_oid: Option<ObjectId>,
1692    pub forced_update: bool,
1693}
1694
1695#[derive(Debug, Clone, PartialEq, Eq)]
1696pub enum ReceivePackCommandStatusV2 {
1697    Ok {
1698        name: String,
1699        options: ReceivePackCommandStatusV2Options,
1700    },
1701    Ng {
1702        name: String,
1703        message: String,
1704    },
1705}
1706
1707#[derive(Debug, Clone, PartialEq, Eq)]
1708pub struct ReceivePackReportStatusV2 {
1709    pub unpack: ReceivePackUnpackStatus,
1710    pub commands: Vec<ReceivePackCommandStatusV2>,
1711}
1712
1713#[derive(Debug, Clone, PartialEq, Eq)]
1714pub struct ProtocolV2CommandRequest {
1715    pub command: String,
1716    pub capabilities: Vec<Capability>,
1717    pub arguments: Vec<Vec<u8>>,
1718}
1719
1720#[derive(Debug, Clone, PartialEq, Eq)]
1721pub enum ProtocolV2Request {
1722    Command(ProtocolV2CommandRequest),
1723    Done,
1724}
1725
1726#[derive(Debug, Clone, PartialEq, Eq)]
1727pub enum ProtocolV2Command {
1728    LsRefs(ProtocolV2LsRefsRequest),
1729    Fetch(ProtocolV2FetchRequest),
1730    ObjectInfo(ProtocolV2ObjectInfoRequest),
1731    Unknown(ProtocolV2CommandRequest),
1732}
1733
1734#[derive(Debug, Clone, PartialEq, Eq)]
1735pub enum ProtocolV2SessionRequest {
1736    Command(ProtocolV2Command),
1737    Done,
1738}
1739
1740#[derive(Debug, Clone, PartialEq, Eq, Default)]
1741pub struct ProtocolV2CommandOptions {
1742    pub agent: Option<String>,
1743    pub object_format: Option<ObjectFormat>,
1744    pub server_options: Vec<String>,
1745    pub extra: Vec<Capability>,
1746}
1747
1748#[derive(Debug, Clone, PartialEq, Eq, Default)]
1749pub struct ProtocolV2FetchFeatures {
1750    pub shallow: bool,
1751    pub wait_for_done: bool,
1752    pub filter: bool,
1753    pub ref_in_want: bool,
1754    pub sideband_all: bool,
1755    pub packfile_uris: bool,
1756    pub unknown: Vec<String>,
1757}
1758
1759#[derive(Debug, Clone, PartialEq, Eq, Default)]
1760pub struct ProtocolV2LsRefsFeatures {
1761    pub unborn: bool,
1762    pub unknown: Vec<String>,
1763}
1764
1765impl ProtocolV2CommandRequest {
1766    pub fn new(command: impl Into<String>) -> Result<Self> {
1767        let command = command.into();
1768        validate_capability_name(&command)?;
1769        Ok(Self {
1770            command,
1771            capabilities: Vec::new(),
1772            arguments: Vec::new(),
1773        })
1774    }
1775}
1776
1777#[derive(Debug, Clone, PartialEq, Eq, Default)]
1778pub struct ProtocolV2LsRefsRequest {
1779    pub peel: bool,
1780    pub symrefs: bool,
1781    pub unborn: bool,
1782    pub ref_prefixes: Vec<String>,
1783}
1784
1785#[derive(Debug, Clone, PartialEq, Eq)]
1786pub struct ProtocolV2LsRefsRef {
1787    pub oid: ObjectId,
1788    pub name: String,
1789    pub peeled: Option<ObjectId>,
1790    pub symref_target: Option<String>,
1791    pub attributes: Vec<String>,
1792}
1793
1794#[derive(Debug, Clone, PartialEq, Eq)]
1795pub enum ProtocolV2LsRefsRecord {
1796    Ref(ProtocolV2LsRefsRef),
1797    Unborn {
1798        name: String,
1799        symref_target: Option<String>,
1800        attributes: Vec<String>,
1801    },
1802}
1803
1804#[derive(Debug, Clone, PartialEq, Eq, Default)]
1805pub struct ProtocolV2FetchRequest {
1806    pub wants: Vec<ObjectId>,
1807    pub want_refs: Vec<String>,
1808    pub haves: Vec<ObjectId>,
1809    pub shallow: Vec<ObjectId>,
1810    pub deepen: Option<u32>,
1811    pub deepen_since: Option<u64>,
1812    pub deepen_not: Vec<String>,
1813    pub deepen_relative: bool,
1814    pub filter: Option<String>,
1815    pub packfile_uris: Option<String>,
1816    pub thin_pack: bool,
1817    pub no_progress: bool,
1818    pub include_tag: bool,
1819    pub ofs_delta: bool,
1820    pub sideband_all: bool,
1821    pub wait_for_done: bool,
1822    pub done: bool,
1823}
1824
1825#[derive(Debug, Clone, PartialEq, Eq)]
1826pub enum ProtocolV2FetchAcknowledgment {
1827    Nak,
1828    Ack(ObjectId),
1829    Ready,
1830}
1831
1832#[derive(Debug, Clone, PartialEq, Eq)]
1833pub enum ProtocolV2FetchShallowInfo {
1834    Shallow(ObjectId),
1835    Unshallow(ObjectId),
1836}
1837
1838#[derive(Debug, Clone, PartialEq, Eq)]
1839pub struct ProtocolV2FetchWantedRef {
1840    pub oid: ObjectId,
1841    pub name: String,
1842}
1843
1844#[derive(Debug, Clone, PartialEq, Eq)]
1845pub struct ProtocolV2FetchPackfileUri {
1846    pub pack_hash: ObjectId,
1847    pub uri: String,
1848}
1849
1850#[derive(Debug, Clone, PartialEq, Eq)]
1851pub enum ProtocolV2FetchResponseSection {
1852    Acknowledgments(Vec<ProtocolV2FetchAcknowledgment>),
1853    ShallowInfo(Vec<ProtocolV2FetchShallowInfo>),
1854    WantedRefs(Vec<ProtocolV2FetchWantedRef>),
1855    PackfileUris(Vec<ProtocolV2FetchPackfileUri>),
1856    Packfile(Vec<Vec<u8>>),
1857    Unknown { name: String, lines: Vec<Vec<u8>> },
1858}
1859
1860#[derive(Debug, Clone, PartialEq, Eq, Default)]
1861pub struct ProtocolV2FetchSidebandAllResponse {
1862    pub sections: Vec<ProtocolV2FetchResponseSection>,
1863    pub progress: Vec<Vec<u8>>,
1864}
1865
1866#[derive(Debug, Clone, PartialEq, Eq, Default)]
1867pub struct ProtocolV2ObjectInfoRequest {
1868    pub size: bool,
1869    pub oids: Vec<ObjectId>,
1870}
1871
1872#[derive(Debug, Clone, PartialEq, Eq)]
1873pub struct ProtocolV2ObjectInfoRecord {
1874    pub oid: ObjectId,
1875    pub size: u64,
1876}
1877
1878#[derive(Debug, Clone, PartialEq, Eq, Default)]
1879pub struct ProtocolV2ObjectInfoResponse {
1880    pub size: bool,
1881    pub records: Vec<ProtocolV2ObjectInfoRecord>,
1882}
1883
1884impl ProtocolV2LsRefsRequest {
1885    pub fn from_command_request(request: &ProtocolV2CommandRequest) -> Result<Self> {
1886        if request.command != "ls-refs" {
1887            return Err(GitError::InvalidFormat(format!(
1888                "expected ls-refs command, got {}",
1889                request.command
1890            )));
1891        }
1892        let mut out = Self::default();
1893        for argument in &request.arguments {
1894            let text = std::str::from_utf8(argument)
1895                .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
1896            match text {
1897                "peel" => out.peel = true,
1898                "symrefs" => out.symrefs = true,
1899                "unborn" => out.unborn = true,
1900                value if value.starts_with("ref-prefix ") => {
1901                    let prefix = value
1902                        .strip_prefix("ref-prefix ")
1903                        .ok_or_else(|| GitError::InvalidFormat("invalid ref-prefix".into()))?;
1904                    validate_protocol_v2_token("ls-refs ref-prefix", prefix)?;
1905                    out.ref_prefixes.push(prefix.to_string());
1906                }
1907                other => {
1908                    return Err(GitError::InvalidFormat(format!(
1909                        "unsupported ls-refs argument {other}"
1910                    )));
1911                }
1912            }
1913        }
1914        Ok(out)
1915    }
1916
1917    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
1918        let mut request = ProtocolV2CommandRequest::new("ls-refs")?;
1919        if self.peel {
1920            request.arguments.push(b"peel".to_vec());
1921        }
1922        if self.symrefs {
1923            request.arguments.push(b"symrefs".to_vec());
1924        }
1925        if self.unborn {
1926            request.arguments.push(b"unborn".to_vec());
1927        }
1928        for prefix in &self.ref_prefixes {
1929            validate_protocol_v2_token("ls-refs ref-prefix", prefix)?;
1930            request
1931                .arguments
1932                .push(format!("ref-prefix {prefix}").into_bytes());
1933        }
1934        Ok(request)
1935    }
1936}
1937
1938impl ProtocolV2FetchRequest {
1939    pub fn from_command_request(
1940        format: ObjectFormat,
1941        request: &ProtocolV2CommandRequest,
1942    ) -> Result<Self> {
1943        if request.command != "fetch" {
1944            return Err(GitError::InvalidFormat(format!(
1945                "expected fetch command, got {}",
1946                request.command
1947            )));
1948        }
1949        let mut out = Self::default();
1950        for argument in &request.arguments {
1951            let text = std::str::from_utf8(argument)
1952                .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
1953            match text {
1954                "thin-pack" => out.thin_pack = true,
1955                "no-progress" => out.no_progress = true,
1956                "include-tag" => out.include_tag = true,
1957                "ofs-delta" => out.ofs_delta = true,
1958                "sideband-all" => out.sideband_all = true,
1959                "wait-for-done" => out.wait_for_done = true,
1960                "deepen-relative" => out.deepen_relative = true,
1961                "done" => out.done = true,
1962                value if value.starts_with("want ") => {
1963                    out.wants
1964                        .push(parse_oid_argument(format, "fetch want", value, "want ")?);
1965                }
1966                value if value.starts_with("want-ref ") => {
1967                    let name = value
1968                        .strip_prefix("want-ref ")
1969                        .ok_or_else(|| GitError::InvalidFormat("invalid fetch want-ref".into()))?;
1970                    validate_protocol_v2_token("fetch want-ref", name)?;
1971                    out.want_refs.push(name.to_string());
1972                }
1973                value if value.starts_with("have ") => {
1974                    out.haves
1975                        .push(parse_oid_argument(format, "fetch have", value, "have ")?);
1976                }
1977                value if value.starts_with("shallow ") => {
1978                    out.shallow.push(parse_oid_argument(
1979                        format,
1980                        "fetch shallow",
1981                        value,
1982                        "shallow ",
1983                    )?);
1984                }
1985                value if value.starts_with("deepen ") => {
1986                    if out.deepen.is_some() {
1987                        return Err(GitError::InvalidFormat(
1988                            "fetch request has duplicate deepen".into(),
1989                        ));
1990                    }
1991                    out.deepen = Some(parse_u32_argument("fetch deepen", value, "deepen ")?);
1992                }
1993                value if value.starts_with("deepen-since ") => {
1994                    if out.deepen_since.is_some() {
1995                        return Err(GitError::InvalidFormat(
1996                            "fetch request has duplicate deepen-since".into(),
1997                        ));
1998                    }
1999                    out.deepen_since = Some(parse_u64_argument(
2000                        "fetch deepen-since",
2001                        value,
2002                        "deepen-since ",
2003                    )?);
2004                }
2005                value if value.starts_with("deepen-not ") => {
2006                    let name = value.strip_prefix("deepen-not ").ok_or_else(|| {
2007                        GitError::InvalidFormat("invalid fetch deepen-not".into())
2008                    })?;
2009                    validate_protocol_v2_token("fetch deepen-not", name)?;
2010                    out.deepen_not.push(name.to_string());
2011                }
2012                value if value.starts_with("filter ") => {
2013                    if out.filter.is_some() {
2014                        return Err(GitError::InvalidFormat(
2015                            "fetch request has duplicate filter".into(),
2016                        ));
2017                    }
2018                    let filter = value
2019                        .strip_prefix("filter ")
2020                        .ok_or_else(|| GitError::InvalidFormat("invalid fetch filter".into()))?;
2021                    validate_protocol_v2_token("fetch filter", filter)?;
2022                    out.filter = Some(filter.to_string());
2023                }
2024                value if value.starts_with("packfile-uris ") => {
2025                    if out.packfile_uris.is_some() {
2026                        return Err(GitError::InvalidFormat(
2027                            "fetch request has duplicate packfile-uris".into(),
2028                        ));
2029                    }
2030                    let protocols = value.strip_prefix("packfile-uris ").ok_or_else(|| {
2031                        GitError::InvalidFormat("invalid fetch packfile-uris".into())
2032                    })?;
2033                    validate_protocol_v2_token("fetch packfile-uris", protocols)?;
2034                    out.packfile_uris = Some(protocols.to_string());
2035                }
2036                other => {
2037                    return Err(GitError::InvalidFormat(format!(
2038                        "unsupported fetch argument {other}"
2039                    )));
2040                }
2041            }
2042        }
2043        Ok(out)
2044    }
2045
2046    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
2047        let mut request = ProtocolV2CommandRequest::new("fetch")?;
2048        for oid in &self.wants {
2049            request.arguments.push(format!("want {oid}").into_bytes());
2050        }
2051        for name in &self.want_refs {
2052            validate_protocol_v2_token("fetch want-ref", name)?;
2053            request
2054                .arguments
2055                .push(format!("want-ref {name}").into_bytes());
2056        }
2057        for oid in &self.haves {
2058            request.arguments.push(format!("have {oid}").into_bytes());
2059        }
2060        for oid in &self.shallow {
2061            request
2062                .arguments
2063                .push(format!("shallow {oid}").into_bytes());
2064        }
2065        if let Some(deepen) = self.deepen {
2066            if deepen == 0 {
2067                return Err(GitError::InvalidFormat(
2068                    "fetch deepen must be positive".into(),
2069                ));
2070            }
2071            request
2072                .arguments
2073                .push(format!("deepen {deepen}").into_bytes());
2074        }
2075        if let Some(deepen_since) = self.deepen_since {
2076            request
2077                .arguments
2078                .push(format!("deepen-since {deepen_since}").into_bytes());
2079        }
2080        for name in &self.deepen_not {
2081            validate_protocol_v2_token("fetch deepen-not", name)?;
2082            request
2083                .arguments
2084                .push(format!("deepen-not {name}").into_bytes());
2085        }
2086        if self.deepen_relative {
2087            request.arguments.push(b"deepen-relative".to_vec());
2088        }
2089        if let Some(filter) = &self.filter {
2090            validate_protocol_v2_token("fetch filter", filter)?;
2091            request
2092                .arguments
2093                .push(format!("filter {filter}").into_bytes());
2094        }
2095        if let Some(protocols) = &self.packfile_uris {
2096            validate_protocol_v2_token("fetch packfile-uris", protocols)?;
2097            request
2098                .arguments
2099                .push(format!("packfile-uris {protocols}").into_bytes());
2100        }
2101        if self.thin_pack {
2102            request.arguments.push(b"thin-pack".to_vec());
2103        }
2104        if self.no_progress {
2105            request.arguments.push(b"no-progress".to_vec());
2106        }
2107        if self.include_tag {
2108            request.arguments.push(b"include-tag".to_vec());
2109        }
2110        if self.ofs_delta {
2111            request.arguments.push(b"ofs-delta".to_vec());
2112        }
2113        if self.sideband_all {
2114            request.arguments.push(b"sideband-all".to_vec());
2115        }
2116        if self.wait_for_done {
2117            request.arguments.push(b"wait-for-done".to_vec());
2118        }
2119        if self.done {
2120            request.arguments.push(b"done".to_vec());
2121        }
2122        Ok(request)
2123    }
2124}
2125
2126impl ProtocolV2ObjectInfoRequest {
2127    pub fn from_command_request(
2128        format: ObjectFormat,
2129        request: &ProtocolV2CommandRequest,
2130    ) -> Result<Self> {
2131        if request.command != "object-info" {
2132            return Err(GitError::InvalidFormat(format!(
2133                "expected object-info command, got {}",
2134                request.command
2135            )));
2136        }
2137        let mut out = Self::default();
2138        for argument in &request.arguments {
2139            let text = parse_protocol_v2_line_text("object-info request argument", argument)?;
2140            if text == "size" {
2141                if out.size {
2142                    return Err(GitError::InvalidFormat(
2143                        "object-info request has duplicate size argument".into(),
2144                    ));
2145                }
2146                out.size = true;
2147            } else if text.starts_with("oid ") {
2148                out.oids
2149                    .push(parse_oid_argument(format, "object-info oid", text, "oid ")?);
2150            } else {
2151                return Err(GitError::InvalidFormat(format!(
2152                    "unsupported object-info request argument {text}"
2153                )));
2154            }
2155        }
2156        if !out.size {
2157            return Err(GitError::InvalidFormat(
2158                "object-info request is missing size argument".into(),
2159            ));
2160        }
2161        if out.oids.is_empty() {
2162            return Err(GitError::InvalidFormat(
2163                "object-info request is missing object ids".into(),
2164            ));
2165        }
2166        Ok(out)
2167    }
2168
2169    pub fn to_command_request(&self) -> Result<ProtocolV2CommandRequest> {
2170        if !self.size {
2171            return Err(GitError::InvalidFormat(
2172                "object-info request is missing size argument".into(),
2173            ));
2174        }
2175        if self.oids.is_empty() {
2176            return Err(GitError::InvalidFormat(
2177                "object-info request is missing object ids".into(),
2178            ));
2179        }
2180        let mut request = ProtocolV2CommandRequest::new("object-info")?;
2181        request.arguments.push(b"size".to_vec());
2182        for oid in &self.oids {
2183            request.arguments.push(format!("oid {oid}").into_bytes());
2184        }
2185        Ok(request)
2186    }
2187}
2188
2189pub fn parse_protocol_v2_advertisement(frames: &[PktLineFrame]) -> Result<TransportHandshake> {
2190    let Some((first, rest)) = frames.split_first() else {
2191        return Err(GitError::InvalidFormat(
2192            "protocol v2 advertisement is empty".into(),
2193        ));
2194    };
2195    match first {
2196        PktLineFrame::Data(payload) if trim_trailing_lf(payload) == b"version 2" => {}
2197        PktLineFrame::Data(_) => {
2198            return Err(GitError::InvalidFormat(
2199                "protocol v2 advertisement missing version line".into(),
2200            ));
2201        }
2202        _ => {
2203            return Err(GitError::InvalidFormat(
2204                "protocol v2 advertisement must start with a data line".into(),
2205            ));
2206        }
2207    }
2208
2209    let mut capabilities = Vec::new();
2210    let mut saw_flush = false;
2211    for (idx, frame) in rest.iter().enumerate() {
2212        match frame {
2213            PktLineFrame::Data(payload) => {
2214                if saw_flush {
2215                    return Err(GitError::InvalidFormat(
2216                        "protocol v2 advertisement has data after flush".into(),
2217                    ));
2218                }
2219                capabilities.push(parse_protocol_v2_capability_line(payload)?);
2220            }
2221            PktLineFrame::Flush => {
2222                saw_flush = true;
2223                if idx + 1 != rest.len() {
2224                    return Err(GitError::InvalidFormat(
2225                        "protocol v2 advertisement has frames after flush".into(),
2226                    ));
2227                }
2228            }
2229            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
2230                return Err(GitError::InvalidFormat(
2231                    "protocol v2 advertisement contains a non-flush control packet".into(),
2232                ));
2233            }
2234        }
2235    }
2236    if !saw_flush {
2237        return Err(GitError::InvalidFormat(
2238            "protocol v2 advertisement missing flush".into(),
2239        ));
2240    }
2241
2242    Ok(TransportHandshake {
2243        protocol: ProtocolVersion::V2,
2244        capabilities,
2245    })
2246}
2247
2248pub fn encode_protocol_v2_advertisement(
2249    handshake: &TransportHandshake,
2250) -> Result<Vec<PktLineFrame>> {
2251    if handshake.protocol != ProtocolVersion::V2 {
2252        return Err(GitError::InvalidFormat(
2253            "protocol v2 advertisement requires a v2 handshake".into(),
2254        ));
2255    }
2256    let mut frames = vec![PktLineFrame::data(line_from_str("version 2"))?];
2257    for capability in &handshake.capabilities {
2258        frames.push(PktLineFrame::data(line(encode_protocol_v2_capability(
2259            capability,
2260        )?))?);
2261    }
2262    frames.push(PktLineFrame::Flush);
2263    Ok(frames)
2264}
2265
2266pub fn read_protocol_v2_advertisement(reader: &mut impl Read) -> Result<TransportHandshake> {
2267    let frames = read_pkt_line_frames_until_flush(reader)?;
2268    parse_protocol_v2_advertisement(&frames)
2269}
2270
2271pub fn write_protocol_v2_advertisement(
2272    writer: &mut impl Write,
2273    handshake: &TransportHandshake,
2274) -> Result<()> {
2275    if handshake.protocol != ProtocolVersion::V2 {
2276        return Err(GitError::InvalidFormat(
2277            "protocol v2 advertisement requires a v2 handshake".into(),
2278        ));
2279    }
2280    write_pkt_line_payload(writer, b"version 2\n")?;
2281    for capability in &handshake.capabilities {
2282        write_pkt_line_payload(writer, &line(encode_protocol_v2_capability(capability)?))?;
2283    }
2284    writer.write_all(b"0000")?;
2285    Ok(())
2286}
2287
2288pub fn parse_protocol_v2_command_request(
2289    frames: &[PktLineFrame],
2290) -> Result<ProtocolV2CommandRequest> {
2291    let Some((first, rest)) = frames.split_first() else {
2292        return Err(GitError::InvalidFormat(
2293            "protocol v2 command request is empty".into(),
2294        ));
2295    };
2296    let command = match first {
2297        PktLineFrame::Data(payload) => parse_protocol_v2_command_line(payload)?,
2298        _ => {
2299            return Err(GitError::InvalidFormat(
2300                "protocol v2 command request must start with a command line".into(),
2301            ));
2302        }
2303    };
2304
2305    let mut capabilities = Vec::new();
2306    let mut arguments = Vec::new();
2307    let mut in_arguments = false;
2308    let mut saw_flush = false;
2309    for (idx, frame) in rest.iter().enumerate() {
2310        match frame {
2311            PktLineFrame::Data(payload) if !in_arguments => {
2312                if saw_flush {
2313                    return Err(GitError::InvalidFormat(
2314                        "protocol v2 command request has data after flush".into(),
2315                    ));
2316                }
2317                capabilities.push(parse_protocol_v2_capability_line(payload)?);
2318            }
2319            PktLineFrame::Data(payload) => {
2320                if saw_flush {
2321                    return Err(GitError::InvalidFormat(
2322                        "protocol v2 command request has data after flush".into(),
2323                    ));
2324                }
2325                let argument = trim_trailing_lf(payload);
2326                if argument.is_empty() {
2327                    return Err(GitError::InvalidFormat(
2328                        "protocol v2 command argument is empty".into(),
2329                    ));
2330                }
2331                if argument
2332                    .iter()
2333                    .any(|byte| matches!(*byte, b'\n' | b'\r' | 0))
2334                {
2335                    return Err(GitError::InvalidFormat(
2336                        "protocol v2 command argument contains a delimiter byte".into(),
2337                    ));
2338                }
2339                arguments.push(argument.to_vec());
2340            }
2341            PktLineFrame::Delimiter => {
2342                if in_arguments {
2343                    return Err(GitError::InvalidFormat(format!(
2344                        "expected flush after {} arguments",
2345                        command
2346                    )));
2347                }
2348                if saw_flush {
2349                    return Err(GitError::InvalidFormat(
2350                        "protocol v2 command request has delimiter after flush".into(),
2351                    ));
2352                }
2353                in_arguments = true;
2354            }
2355            PktLineFrame::Flush => {
2356                saw_flush = true;
2357                if idx + 1 != rest.len() {
2358                    return Err(GitError::InvalidFormat(
2359                        "protocol v2 command request has frames after flush".into(),
2360                    ));
2361                }
2362            }
2363            PktLineFrame::ResponseEnd => {
2364                return Err(GitError::InvalidFormat(
2365                    "protocol v2 command request contains response-end".into(),
2366                ));
2367            }
2368        }
2369    }
2370    if !saw_flush {
2371        return Err(GitError::InvalidFormat(
2372            "protocol v2 command request missing flush".into(),
2373        ));
2374    }
2375
2376    Ok(ProtocolV2CommandRequest {
2377        command,
2378        capabilities,
2379        arguments,
2380    })
2381}
2382
2383pub fn encode_protocol_v2_command_request(
2384    request: &ProtocolV2CommandRequest,
2385) -> Result<Vec<PktLineFrame>> {
2386    validate_capability_name(&request.command)?;
2387    let mut frames = Vec::new();
2388    frames.push(PktLineFrame::data(line_from_str(&format!(
2389        "command={}",
2390        request.command
2391    )))?);
2392    for capability in &request.capabilities {
2393        frames.push(PktLineFrame::data(line(encode_protocol_v2_capability(
2394            capability,
2395        )?))?);
2396    }
2397    if !request.arguments.is_empty() {
2398        frames.push(PktLineFrame::Delimiter);
2399        for argument in &request.arguments {
2400            validate_protocol_v2_argument(argument)?;
2401            let mut payload = argument.clone();
2402            payload.push(b'\n');
2403            frames.push(PktLineFrame::data(payload)?);
2404        }
2405    }
2406    frames.push(PktLineFrame::Flush);
2407    Ok(frames)
2408}
2409
2410pub fn parse_protocol_v2_request(frames: &[PktLineFrame]) -> Result<ProtocolV2Request> {
2411    if matches!(frames, [PktLineFrame::Flush]) {
2412        return Ok(ProtocolV2Request::Done);
2413    }
2414    parse_protocol_v2_command_request(frames).map(ProtocolV2Request::Command)
2415}
2416
2417pub fn encode_protocol_v2_request(request: &ProtocolV2Request) -> Result<Vec<PktLineFrame>> {
2418    match request {
2419        ProtocolV2Request::Command(command) => encode_protocol_v2_command_request(command),
2420        ProtocolV2Request::Done => Ok(vec![PktLineFrame::Flush]),
2421    }
2422}
2423
2424pub fn read_protocol_v2_request(reader: &mut impl Read) -> Result<ProtocolV2Request> {
2425    let frames = read_pkt_line_frames_until_flush(reader)?;
2426    parse_protocol_v2_request(&frames)
2427}
2428
2429pub fn write_protocol_v2_request(
2430    writer: &mut impl Write,
2431    request: &ProtocolV2Request,
2432) -> Result<()> {
2433    match request {
2434        ProtocolV2Request::Command(command) => write_protocol_v2_command_request(writer, command),
2435        ProtocolV2Request::Done => {
2436            writer.write_all(b"0000")?;
2437            Ok(())
2438        }
2439    }
2440}
2441
2442pub fn read_protocol_v2_command_request(
2443    reader: &mut impl Read,
2444) -> Result<ProtocolV2CommandRequest> {
2445    let mut frames = Vec::new();
2446    loop {
2447        let Some(frame) = read_pkt_line_frame(reader)? else {
2448            if let Some(command) = frames.first().and_then(|frame| match frame {
2449                PktLineFrame::Data(payload) => parse_protocol_v2_command_line(payload).ok(),
2450                _ => None,
2451            }) && frames
2452                .iter()
2453                .any(|frame| matches!(frame, PktLineFrame::Delimiter))
2454            {
2455                return Err(GitError::InvalidFormat(format!(
2456                    "expected flush after {} arguments",
2457                    command
2458                )));
2459            }
2460            return Err(GitError::InvalidFormat(
2461                "pkt-line stream ended before control packet".into(),
2462            ));
2463        };
2464        let done = matches!(frame, PktLineFrame::Flush);
2465        frames.push(frame);
2466        if done {
2467            break;
2468        }
2469    }
2470    parse_protocol_v2_command_request(&frames)
2471}
2472
2473pub fn write_protocol_v2_command_request(
2474    writer: &mut impl Write,
2475    request: &ProtocolV2CommandRequest,
2476) -> Result<()> {
2477    validate_capability_name(&request.command)?;
2478    write_pkt_line_payload(
2479        writer,
2480        &line_from_str(&format!("command={}", request.command)),
2481    )?;
2482    for capability in &request.capabilities {
2483        write_pkt_line_payload(writer, &line(encode_protocol_v2_capability(capability)?))?;
2484    }
2485    if !request.arguments.is_empty() {
2486        write_pkt_line_frame(writer, &PktLineFrame::Delimiter)?;
2487        for argument in &request.arguments {
2488            validate_protocol_v2_argument(argument)?;
2489            let mut payload = argument.clone();
2490            payload.push(b'\n');
2491            write_pkt_line_payload(writer, &payload)?;
2492        }
2493    }
2494    writer.write_all(b"0000")?;
2495    Ok(())
2496}
2497
2498pub fn read_protocol_v2_ls_refs_request(reader: &mut impl Read) -> Result<ProtocolV2LsRefsRequest> {
2499    let request = read_protocol_v2_command_request(reader)?;
2500    ProtocolV2LsRefsRequest::from_command_request(&request)
2501}
2502
2503pub fn write_protocol_v2_ls_refs_request(
2504    writer: &mut impl Write,
2505    request: &ProtocolV2LsRefsRequest,
2506) -> Result<()> {
2507    let command = request.to_command_request()?;
2508    write_protocol_v2_command_request(writer, &command)
2509}
2510
2511pub fn parse_protocol_v2_ls_refs_response(
2512    format: ObjectFormat,
2513    frames: &[PktLineFrame],
2514) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2515    let mut records = Vec::new();
2516    let mut saw_flush = false;
2517    for (idx, frame) in frames.iter().enumerate() {
2518        match frame {
2519            PktLineFrame::Data(payload) => {
2520                if saw_flush {
2521                    return Err(GitError::InvalidFormat(
2522                        "ls-refs response has data after flush".into(),
2523                    ));
2524                }
2525                records.push(parse_protocol_v2_ls_refs_line(format, payload)?);
2526            }
2527            PktLineFrame::Flush => {
2528                saw_flush = true;
2529                if !flush_terminates_protocol_v2_response(frames, idx) {
2530                    return Err(GitError::InvalidFormat(
2531                        "ls-refs response has frames after flush".into(),
2532                    ));
2533                }
2534            }
2535            PktLineFrame::ResponseEnd if saw_flush && idx + 1 == frames.len() => {}
2536            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
2537                return Err(GitError::InvalidFormat(
2538                    "ls-refs response contains a non-flush control packet".into(),
2539                ));
2540            }
2541        }
2542    }
2543    if !saw_flush {
2544        return Err(GitError::InvalidFormat(
2545            "ls-refs response missing flush".into(),
2546        ));
2547    }
2548    Ok(records)
2549}
2550
2551pub fn encode_protocol_v2_ls_refs_response(
2552    records: &[ProtocolV2LsRefsRecord],
2553) -> Result<Vec<PktLineFrame>> {
2554    let mut frames = Vec::new();
2555    for record in records {
2556        frames.push(PktLineFrame::data(line_from_str(
2557            &format_protocol_v2_ls_refs_record(record)?,
2558        ))?);
2559    }
2560    frames.push(PktLineFrame::Flush);
2561    Ok(frames)
2562}
2563
2564pub fn read_protocol_v2_ls_refs_response(
2565    format: ObjectFormat,
2566    reader: &mut impl Read,
2567) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2568    let frames = read_pkt_line_frames_until_flush(reader)?;
2569    parse_protocol_v2_ls_refs_response(format, &frames)
2570}
2571
2572pub fn write_protocol_v2_ls_refs_response(
2573    writer: &mut impl Write,
2574    records: &[ProtocolV2LsRefsRecord],
2575) -> Result<()> {
2576    for record in records {
2577        write_pkt_line_payload(
2578            writer,
2579            &line_from_str(&format_protocol_v2_ls_refs_record(record)?),
2580        )?;
2581    }
2582    writer.write_all(b"0000")?;
2583    Ok(())
2584}
2585
2586pub fn read_protocol_v2_ls_refs_response_until_response_end(
2587    format: ObjectFormat,
2588    reader: &mut impl Read,
2589) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2590    let frames = read_pkt_line_frames_until_response_end(reader)?;
2591    parse_protocol_v2_ls_refs_response(format, &frames)
2592}
2593
2594pub fn write_protocol_v2_ls_refs_response_with_response_end(
2595    writer: &mut impl Write,
2596    records: &[ProtocolV2LsRefsRecord],
2597) -> Result<()> {
2598    write_protocol_v2_ls_refs_response(writer, records)?;
2599    writer.write_all(b"0002")?;
2600    Ok(())
2601}
2602
2603pub fn exchange_protocol_v2_ls_refs(
2604    format: ObjectFormat,
2605    reader: &mut impl Read,
2606    writer: &mut impl Write,
2607    request: &ProtocolV2LsRefsRequest,
2608) -> Result<Vec<ProtocolV2LsRefsRecord>> {
2609    write_protocol_v2_ls_refs_request(writer, request)?;
2610    writer.flush()?;
2611    read_protocol_v2_ls_refs_response(format, reader)
2612}
2613
2614/// Bridge a parsed protocol v2 `ls-refs` response into the shared
2615/// [`RefAdvertisementSet`]/[`RefAdvertisement`] types used by the v0/v1 codecs,
2616/// so callers can drive v2 clone/fetch through the same ref-advertisement
2617/// machinery.
2618///
2619/// Each [`ProtocolV2LsRefsRecord::Ref`] becomes a [`RefAdvertisement`]. A
2620/// `peeled:<oid>` attribute is emitted as an additional `<peeled-oid>
2621/// <name>^{}` advertisement, matching the v0/v1 peeled-tag convention.
2622/// `symref-target:<target>` attributes are collected as `symref=<name>:<target>`
2623/// capabilities on the first advertised ref, mirroring how the upload-pack v0/v1
2624/// advertisement carries symrefs. [`ProtocolV2LsRefsRecord::Unborn`] records have
2625/// no object id, so they cannot be represented as a [`RefAdvertisement`]; an
2626/// unborn record carrying a `symref-target` is preserved as a `symref` capability
2627/// while otherwise being skipped. The returned set always reports
2628/// [`ProtocolVersion::V2`].
2629pub fn protocol_v2_ls_refs_records_to_ref_advertisement_set(
2630    records: &[ProtocolV2LsRefsRecord],
2631) -> Result<RefAdvertisementSet> {
2632    let mut refs: Vec<RefAdvertisement> = Vec::new();
2633    let mut symrefs: Vec<Capability> = Vec::new();
2634    for record in records {
2635        match record {
2636            ProtocolV2LsRefsRecord::Ref(reference) => {
2637                validate_protocol_v2_token("ls-refs ref name", &reference.name)?;
2638                refs.push(RefAdvertisement {
2639                    oid: reference.oid,
2640                    name: reference.name.clone(),
2641                    capabilities: Vec::new(),
2642                });
2643                if let Some(peeled) = &reference.peeled {
2644                    refs.push(RefAdvertisement {
2645                        oid: peeled.clone(),
2646                        name: format!("{}^{{}}", reference.name),
2647                        capabilities: Vec::new(),
2648                    });
2649                }
2650                if let Some(target) = &reference.symref_target {
2651                    symrefs.push(protocol_v2_symref_capability(&reference.name, target)?);
2652                }
2653            }
2654            ProtocolV2LsRefsRecord::Unborn {
2655                name,
2656                symref_target,
2657                ..
2658            } => {
2659                validate_protocol_v2_token("ls-refs ref name", name)?;
2660                if let Some(target) = symref_target {
2661                    symrefs.push(protocol_v2_symref_capability(name, target)?);
2662                }
2663            }
2664        }
2665    }
2666    if !symrefs.is_empty() {
2667        if let Some(first) = refs.first_mut() {
2668            first.capabilities = symrefs;
2669        } else {
2670            return Err(GitError::InvalidFormat(
2671                "ls-refs response advertised symrefs without any concrete refs".into(),
2672            ));
2673        }
2674    }
2675    Ok(RefAdvertisementSet {
2676        protocol: ProtocolVersion::V2,
2677        refs,
2678        shallow: Vec::new(),
2679    })
2680}
2681
2682/// Parse a protocol v2 `ls-refs` response and bridge it into the shared
2683/// [`RefAdvertisementSet`] type. Convenience wrapper combining
2684/// [`parse_protocol_v2_ls_refs_response`] and
2685/// [`protocol_v2_ls_refs_records_to_ref_advertisement_set`].
2686pub fn parse_protocol_v2_ls_refs_response_as_ref_advertisement_set(
2687    format: ObjectFormat,
2688    frames: &[PktLineFrame],
2689) -> Result<RefAdvertisementSet> {
2690    let records = parse_protocol_v2_ls_refs_response(format, frames)?;
2691    protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
2692}
2693
2694/// Read a protocol v2 `ls-refs` response from `reader` and bridge it into the
2695/// shared [`RefAdvertisementSet`] type.
2696pub fn read_protocol_v2_ls_refs_response_as_ref_advertisement_set(
2697    format: ObjectFormat,
2698    reader: &mut impl Read,
2699) -> Result<RefAdvertisementSet> {
2700    let records = read_protocol_v2_ls_refs_response(format, reader)?;
2701    protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
2702}
2703
2704fn protocol_v2_symref_capability(name: &str, target: &str) -> Result<Capability> {
2705    validate_protocol_v2_token("ls-refs symref-target", target)?;
2706    Ok(Capability {
2707        name: "symref".into(),
2708        value: Some(format!("{name}:{target}")),
2709    })
2710}
2711
2712pub fn read_protocol_v2_fetch_request(
2713    format: ObjectFormat,
2714    reader: &mut impl Read,
2715) -> Result<ProtocolV2FetchRequest> {
2716    let request = read_protocol_v2_command_request(reader)?;
2717    ProtocolV2FetchRequest::from_command_request(format, &request)
2718}
2719
2720pub fn write_protocol_v2_fetch_request(
2721    writer: &mut impl Write,
2722    request: &ProtocolV2FetchRequest,
2723) -> Result<()> {
2724    let command = request.to_command_request()?;
2725    write_protocol_v2_command_request(writer, &command)
2726}
2727
2728pub fn read_protocol_v2_object_info_request(
2729    format: ObjectFormat,
2730    reader: &mut impl Read,
2731) -> Result<ProtocolV2ObjectInfoRequest> {
2732    let request = read_protocol_v2_command_request(reader)?;
2733    ProtocolV2ObjectInfoRequest::from_command_request(format, &request)
2734}
2735
2736pub fn write_protocol_v2_object_info_request(
2737    writer: &mut impl Write,
2738    request: &ProtocolV2ObjectInfoRequest,
2739) -> Result<()> {
2740    let command = request.to_command_request()?;
2741    write_protocol_v2_command_request(writer, &command)
2742}
2743
2744pub fn parse_protocol_v2_fetch_response(
2745    format: ObjectFormat,
2746    frames: &[PktLineFrame],
2747) -> Result<Vec<ProtocolV2FetchResponseSection>> {
2748    let mut sections = Vec::new();
2749    let mut current: Option<(String, Vec<Vec<u8>>)> = None;
2750    let mut saw_flush = false;
2751    for (idx, frame) in frames.iter().enumerate() {
2752        match frame {
2753            PktLineFrame::Data(payload) => {
2754                if saw_flush {
2755                    return Err(GitError::InvalidFormat(
2756                        "fetch response has data after flush".into(),
2757                    ));
2758                }
2759                if let Some((_name, lines)) = &mut current {
2760                    lines.push(payload.clone());
2761                } else {
2762                    let name = parse_fetch_section_header(payload)?;
2763                    current = Some((name, Vec::new()));
2764                }
2765            }
2766            PktLineFrame::Delimiter => {
2767                if saw_flush {
2768                    return Err(GitError::InvalidFormat(
2769                        "fetch response has delimiter after flush".into(),
2770                    ));
2771                }
2772                let Some((name, lines)) = current.take() else {
2773                    return Err(GitError::InvalidFormat(
2774                        "fetch response has delimiter before section".into(),
2775                    ));
2776                };
2777                sections.push(parse_fetch_section(format, name, lines)?);
2778            }
2779            PktLineFrame::Flush => {
2780                saw_flush = true;
2781                if !flush_terminates_protocol_v2_response(frames, idx) {
2782                    return Err(GitError::InvalidFormat(
2783                        "fetch response has frames after flush".into(),
2784                    ));
2785                }
2786                if let Some((name, lines)) = current.take() {
2787                    sections.push(parse_fetch_section(format, name, lines)?);
2788                }
2789            }
2790            PktLineFrame::ResponseEnd if saw_flush && idx + 1 == frames.len() => {}
2791            PktLineFrame::ResponseEnd => {
2792                return Err(GitError::InvalidFormat(
2793                    "fetch response contains response-end".into(),
2794                ));
2795            }
2796        }
2797    }
2798    if !saw_flush {
2799        return Err(GitError::InvalidFormat(
2800            "fetch response missing flush".into(),
2801        ));
2802    }
2803    Ok(sections)
2804}
2805
2806pub fn encode_protocol_v2_fetch_response(
2807    sections: &[ProtocolV2FetchResponseSection],
2808) -> Result<Vec<PktLineFrame>> {
2809    let mut frames = Vec::new();
2810    for (idx, section) in sections.iter().enumerate() {
2811        if idx != 0 {
2812            frames.push(PktLineFrame::Delimiter);
2813        }
2814        frames.push(PktLineFrame::data(line_from_str(
2815            protocol_v2_fetch_section_name(section),
2816        ))?);
2817        for line in format_protocol_v2_fetch_section_lines(section)? {
2818            frames.push(PktLineFrame::data(line)?);
2819        }
2820    }
2821    frames.push(PktLineFrame::Flush);
2822    Ok(frames)
2823}
2824
2825pub fn parse_protocol_v2_fetch_sideband_all_response(
2826    format: ObjectFormat,
2827    frames: &[PktLineFrame],
2828) -> Result<ProtocolV2FetchSidebandAllResponse> {
2829    let mut demuxed = Vec::new();
2830    let mut progress = Vec::new();
2831    let mut in_packfile = false;
2832    for frame in frames {
2833        match frame {
2834            PktLineFrame::Data(payload) if in_packfile => {
2835                demuxed.push(PktLineFrame::Data(payload.clone()));
2836            }
2837            PktLineFrame::Data(payload) => {
2838                let packet = parse_sideband_packet(payload)?;
2839                match packet.channel {
2840                    SideBandChannel::Data => {
2841                        if trim_trailing_lf(&packet.data) == b"packfile" {
2842                            in_packfile = true;
2843                        }
2844                        demuxed.push(PktLineFrame::Data(packet.data));
2845                    }
2846                    SideBandChannel::Progress => progress.push(packet.data),
2847                    SideBandChannel::Fatal => {
2848                        let message = String::from_utf8_lossy(&packet.data).into_owned();
2849                        return Err(GitError::InvalidFormat(format!(
2850                            "sideband fatal: {message}"
2851                        )));
2852                    }
2853                }
2854            }
2855            PktLineFrame::Delimiter => {
2856                in_packfile = false;
2857                demuxed.push(PktLineFrame::Delimiter);
2858            }
2859            PktLineFrame::Flush => {
2860                in_packfile = false;
2861                demuxed.push(PktLineFrame::Flush);
2862            }
2863            PktLineFrame::ResponseEnd => {
2864                in_packfile = false;
2865                demuxed.push(PktLineFrame::ResponseEnd);
2866            }
2867        }
2868    }
2869    Ok(ProtocolV2FetchSidebandAllResponse {
2870        sections: parse_protocol_v2_fetch_response(format, &demuxed)?,
2871        progress,
2872    })
2873}
2874
2875pub fn encode_protocol_v2_fetch_sideband_all_response(
2876    sections: &[ProtocolV2FetchResponseSection],
2877) -> Result<Vec<PktLineFrame>> {
2878    let frames = encode_protocol_v2_fetch_response(sections)?;
2879    let mut encoded = Vec::new();
2880    let mut in_packfile = false;
2881    for frame in frames {
2882        match frame {
2883            PktLineFrame::Data(payload) if in_packfile => {
2884                encoded.push(PktLineFrame::Data(payload));
2885            }
2886            PktLineFrame::Data(payload) => {
2887                if trim_trailing_lf(&payload) == b"packfile" {
2888                    in_packfile = true;
2889                }
2890                encoded.push(PktLineFrame::data(encode_sideband_packet(
2891                    &SideBandPacket {
2892                        channel: SideBandChannel::Data,
2893                        data: payload,
2894                    },
2895                )?)?);
2896            }
2897            PktLineFrame::Delimiter => {
2898                in_packfile = false;
2899                encoded.push(PktLineFrame::Delimiter);
2900            }
2901            PktLineFrame::Flush => {
2902                in_packfile = false;
2903                encoded.push(PktLineFrame::Flush);
2904            }
2905            PktLineFrame::ResponseEnd => {
2906                in_packfile = false;
2907                encoded.push(PktLineFrame::ResponseEnd);
2908            }
2909        }
2910    }
2911    Ok(encoded)
2912}
2913
2914pub fn read_protocol_v2_fetch_response(
2915    format: ObjectFormat,
2916    reader: &mut impl Read,
2917) -> Result<Vec<ProtocolV2FetchResponseSection>> {
2918    let frames = read_pkt_line_frames_until_flush(reader)?;
2919    parse_protocol_v2_fetch_response(format, &frames)
2920}
2921
2922pub fn write_protocol_v2_fetch_response(
2923    writer: &mut impl Write,
2924    sections: &[ProtocolV2FetchResponseSection],
2925) -> Result<()> {
2926    write_protocol_v2_fetch_response_inner(writer, sections, false, false)
2927}
2928
2929pub fn read_protocol_v2_fetch_sideband_all_response(
2930    format: ObjectFormat,
2931    reader: &mut impl Read,
2932) -> Result<ProtocolV2FetchSidebandAllResponse> {
2933    let frames = read_pkt_line_frames_until_flush(reader)?;
2934    parse_protocol_v2_fetch_sideband_all_response(format, &frames)
2935}
2936
2937pub fn write_protocol_v2_fetch_sideband_all_response(
2938    writer: &mut impl Write,
2939    sections: &[ProtocolV2FetchResponseSection],
2940) -> Result<()> {
2941    write_protocol_v2_fetch_response_inner(writer, sections, true, false)
2942}
2943
2944pub fn read_protocol_v2_fetch_response_until_response_end(
2945    format: ObjectFormat,
2946    reader: &mut impl Read,
2947) -> Result<Vec<ProtocolV2FetchResponseSection>> {
2948    let frames = read_pkt_line_frames_until_response_end(reader)?;
2949    parse_protocol_v2_fetch_response(format, &frames)
2950}
2951
2952pub fn write_protocol_v2_fetch_response_with_response_end(
2953    writer: &mut impl Write,
2954    sections: &[ProtocolV2FetchResponseSection],
2955) -> Result<()> {
2956    write_protocol_v2_fetch_response_inner(writer, sections, false, true)
2957}
2958
2959pub fn read_protocol_v2_fetch_sideband_all_response_until_response_end(
2960    format: ObjectFormat,
2961    reader: &mut impl Read,
2962) -> Result<ProtocolV2FetchSidebandAllResponse> {
2963    let frames = read_pkt_line_frames_until_response_end(reader)?;
2964    parse_protocol_v2_fetch_sideband_all_response(format, &frames)
2965}
2966
2967pub fn write_protocol_v2_fetch_sideband_all_response_with_response_end(
2968    writer: &mut impl Write,
2969    sections: &[ProtocolV2FetchResponseSection],
2970) -> Result<()> {
2971    write_protocol_v2_fetch_response_inner(writer, sections, true, true)
2972}
2973
2974fn write_protocol_v2_fetch_response_inner(
2975    writer: &mut impl Write,
2976    sections: &[ProtocolV2FetchResponseSection],
2977    sideband_all: bool,
2978    response_end: bool,
2979) -> Result<()> {
2980    let mut in_packfile = false;
2981    for (idx, section) in sections.iter().enumerate() {
2982        if idx != 0 {
2983            in_packfile = false;
2984            write_pkt_line_frame(writer, &PktLineFrame::Delimiter)?;
2985        }
2986        write_protocol_v2_fetch_payload(
2987            writer,
2988            &line_from_str(protocol_v2_fetch_section_name(section)),
2989            sideband_all,
2990            &mut in_packfile,
2991        )?;
2992        for payload in format_protocol_v2_fetch_section_lines(section)? {
2993            write_protocol_v2_fetch_payload(writer, &payload, sideband_all, &mut in_packfile)?;
2994        }
2995    }
2996    writer.write_all(b"0000")?;
2997    if response_end {
2998        writer.write_all(b"0002")?;
2999    }
3000    Ok(())
3001}
3002
3003fn write_protocol_v2_fetch_payload(
3004    writer: &mut impl Write,
3005    payload: &[u8],
3006    sideband_all: bool,
3007    in_packfile: &mut bool,
3008) -> Result<()> {
3009    if sideband_all && !*in_packfile {
3010        if trim_trailing_lf(payload) == b"packfile" {
3011            *in_packfile = true;
3012        }
3013        write_sideband_payload(writer, SideBandChannel::Data, payload)
3014    } else {
3015        write_pkt_line_payload(writer, payload)
3016    }
3017}
3018
3019pub fn exchange_protocol_v2_fetch(
3020    format: ObjectFormat,
3021    reader: &mut impl Read,
3022    writer: &mut impl Write,
3023    request: &ProtocolV2FetchRequest,
3024) -> Result<Vec<ProtocolV2FetchResponseSection>> {
3025    write_protocol_v2_fetch_request(writer, request)?;
3026    writer.flush()?;
3027    read_protocol_v2_fetch_response(format, reader)
3028}
3029
3030pub fn parse_protocol_v2_object_info_response(
3031    format: ObjectFormat,
3032    frames: &[PktLineFrame],
3033) -> Result<ProtocolV2ObjectInfoResponse> {
3034    let Some((first, rest)) = frames.split_first() else {
3035        return Err(GitError::InvalidFormat(
3036            "object-info response is empty".into(),
3037        ));
3038    };
3039    let PktLineFrame::Data(attrs) = first else {
3040        return Err(GitError::InvalidFormat(
3041            "object-info response must start with attributes".into(),
3042        ));
3043    };
3044    let attrs = parse_protocol_v2_line_text("object-info response attributes", attrs)?;
3045    let mut response = ProtocolV2ObjectInfoResponse::default();
3046    for attr in attrs.split(' ') {
3047        validate_protocol_v2_token("object-info response attribute", attr)?;
3048        match attr {
3049            "size" => {
3050                if response.size {
3051                    return Err(GitError::InvalidFormat(
3052                        "object-info response has duplicate size attribute".into(),
3053                    ));
3054                }
3055                response.size = true;
3056            }
3057            other => {
3058                return Err(GitError::InvalidFormat(format!(
3059                    "unsupported object-info response attribute {other}"
3060                )));
3061            }
3062        }
3063    }
3064    if !response.size {
3065        return Err(GitError::InvalidFormat(
3066            "object-info response is missing size attribute".into(),
3067        ));
3068    }
3069
3070    let mut saw_flush = false;
3071    for (idx, frame) in rest.iter().enumerate() {
3072        match frame {
3073            PktLineFrame::Data(payload) if !saw_flush => {
3074                response
3075                    .records
3076                    .push(parse_protocol_v2_object_info_record(format, payload)?);
3077            }
3078            PktLineFrame::Data(_) => {
3079                return Err(GitError::InvalidFormat(
3080                    "object-info response has data after flush".into(),
3081                ));
3082            }
3083            PktLineFrame::Flush => {
3084                saw_flush = true;
3085                if idx + 1 != rest.len() {
3086                    return Err(GitError::InvalidFormat(
3087                        "object-info response has frames after flush".into(),
3088                    ));
3089                }
3090            }
3091            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
3092                return Err(GitError::InvalidFormat(
3093                    "object-info response contains a non-flush control packet".into(),
3094                ));
3095            }
3096        }
3097    }
3098    if !saw_flush {
3099        return Err(GitError::InvalidFormat(
3100            "object-info response missing flush".into(),
3101        ));
3102    }
3103    Ok(response)
3104}
3105
3106pub fn encode_protocol_v2_object_info_response(
3107    response: &ProtocolV2ObjectInfoResponse,
3108) -> Result<Vec<PktLineFrame>> {
3109    if !response.size {
3110        return Err(GitError::InvalidFormat(
3111            "object-info response is missing size attribute".into(),
3112        ));
3113    }
3114    let mut frames = Vec::new();
3115    frames.push(PktLineFrame::data(line_from_str("size"))?);
3116    for record in &response.records {
3117        frames.push(PktLineFrame::data(line_from_str(&format!(
3118            "{} {}",
3119            record.oid, record.size
3120        )))?);
3121    }
3122    frames.push(PktLineFrame::Flush);
3123    Ok(frames)
3124}
3125
3126pub fn read_protocol_v2_object_info_response(
3127    format: ObjectFormat,
3128    reader: &mut impl Read,
3129) -> Result<ProtocolV2ObjectInfoResponse> {
3130    let frames = read_pkt_line_frames_until_flush(reader)?;
3131    parse_protocol_v2_object_info_response(format, &frames)
3132}
3133
3134pub fn write_protocol_v2_object_info_response(
3135    writer: &mut impl Write,
3136    response: &ProtocolV2ObjectInfoResponse,
3137) -> Result<()> {
3138    if !response.size {
3139        return Err(GitError::InvalidFormat(
3140            "object-info response is missing size attribute".into(),
3141        ));
3142    }
3143    write_pkt_line_payload(writer, b"size\n")?;
3144    for record in &response.records {
3145        write_pkt_line_payload(
3146            writer,
3147            &line_from_str(&format!("{} {}", record.oid, record.size)),
3148        )?;
3149    }
3150    writer.write_all(b"0000")?;
3151    Ok(())
3152}
3153
3154pub fn exchange_protocol_v2_object_info(
3155    format: ObjectFormat,
3156    reader: &mut impl Read,
3157    writer: &mut impl Write,
3158    request: &ProtocolV2ObjectInfoRequest,
3159) -> Result<ProtocolV2ObjectInfoResponse> {
3160    write_protocol_v2_object_info_request(writer, request)?;
3161    writer.flush()?;
3162    read_protocol_v2_object_info_response(format, reader)
3163}
3164
3165pub fn demux_protocol_v2_fetch_packfile(
3166    sections: &[ProtocolV2FetchResponseSection],
3167) -> Result<Option<SideBandDemux>> {
3168    let mut packfile = None;
3169    for section in sections {
3170        if let ProtocolV2FetchResponseSection::Packfile(lines) = section {
3171            if packfile.is_some() {
3172                return Err(GitError::InvalidFormat(
3173                    "fetch response has duplicate packfile sections".into(),
3174                ));
3175            }
3176            packfile = Some(parse_and_demux_sideband_packets(lines)?);
3177        }
3178    }
3179    Ok(packfile)
3180}
3181
3182pub fn protocol_v2_object_format(capabilities: &[Capability]) -> Result<ObjectFormat> {
3183    let mut format = None;
3184    for capability in capabilities {
3185        if capability.name != "object-format" {
3186            continue;
3187        }
3188        if format.is_some() {
3189            return Err(GitError::InvalidFormat(
3190                "protocol v2 has duplicate object-format capabilities".into(),
3191            ));
3192        }
3193        let Some(value) = &capability.value else {
3194            return Err(GitError::InvalidFormat(
3195                "protocol v2 object-format capability is missing a value".into(),
3196            ));
3197        };
3198        format = Some(value.parse::<ObjectFormat>()?);
3199    }
3200    Ok(format.unwrap_or(ObjectFormat::Sha1))
3201}
3202
3203pub fn validate_protocol_v2_command_request_capabilities(
3204    handshake: &TransportHandshake,
3205    request: &ProtocolV2CommandRequest,
3206) -> Result<()> {
3207    if handshake.protocol != ProtocolVersion::V2 {
3208        return Err(GitError::InvalidFormat(
3209            "protocol v2 command validation requires a v2 handshake".into(),
3210        ));
3211    }
3212    let advertised =
3213        protocol_v2_capability(&handshake.capabilities, &request.command).ok_or_else(|| {
3214            GitError::InvalidFormat(format!("unadvertised command {}", request.command))
3215        })?;
3216    if advertised.name.is_empty() {
3217        return Err(GitError::InvalidFormat(
3218            "advertised command capability is empty".into(),
3219        ));
3220    }
3221    parse_protocol_v2_command_options(&request.capabilities)?;
3222
3223    for capability in &request.capabilities {
3224        let advertised = protocol_v2_capability(&handshake.capabilities, &capability.name)
3225            .ok_or_else(|| {
3226                GitError::InvalidFormat(format!(
3227                    "unadvertised protocol v2 capability {}",
3228                    capability.name
3229                ))
3230            })?;
3231        if capability.name == "object-format" {
3232            validate_protocol_v2_object_format_request(advertised, capability)?;
3233        }
3234    }
3235    Ok(())
3236}
3237
3238pub fn parse_protocol_v2_command_options(
3239    capabilities: &[Capability],
3240) -> Result<ProtocolV2CommandOptions> {
3241    let mut out = ProtocolV2CommandOptions::default();
3242    for capability in capabilities {
3243        match capability.name.as_str() {
3244            "agent" => {
3245                if out.agent.is_some() {
3246                    return Err(GitError::InvalidFormat(
3247                        "protocol v2 command has duplicate agent capabilities".into(),
3248                    ));
3249                }
3250                let Some(value) = &capability.value else {
3251                    return Err(GitError::InvalidFormat(
3252                        "protocol v2 agent capability is missing a value".into(),
3253                    ));
3254                };
3255                validate_protocol_v2_capability_value(value)?;
3256                out.agent = Some(value.clone());
3257            }
3258            "object-format" => {
3259                if out.object_format.is_some() {
3260                    return Err(GitError::InvalidFormat(
3261                        "protocol v2 command has duplicate object-format capabilities".into(),
3262                    ));
3263                }
3264                let Some(value) = &capability.value else {
3265                    return Err(GitError::InvalidFormat(
3266                        "protocol v2 object-format capability is missing a value".into(),
3267                    ));
3268                };
3269                out.object_format = Some(value.parse::<ObjectFormat>()?);
3270            }
3271            "server-option" => {
3272                let Some(value) = &capability.value else {
3273                    return Err(GitError::InvalidFormat(
3274                        "protocol v2 server-option capability is missing a value".into(),
3275                    ));
3276                };
3277                validate_protocol_v2_capability_value(value)?;
3278                out.server_options.push(value.clone());
3279            }
3280            _ => out.extra.push(capability.clone()),
3281        }
3282    }
3283    Ok(out)
3284}
3285
3286pub fn encode_protocol_v2_command_options(
3287    options: &ProtocolV2CommandOptions,
3288) -> Result<Vec<Capability>> {
3289    let mut capabilities = Vec::new();
3290    if let Some(agent) = &options.agent {
3291        validate_protocol_v2_capability_value(agent)?;
3292        capabilities.push(Capability {
3293            name: "agent".into(),
3294            value: Some(agent.clone()),
3295        });
3296    }
3297    if let Some(format) = options.object_format {
3298        capabilities.push(Capability {
3299            name: "object-format".into(),
3300            value: Some(format.name().into()),
3301        });
3302    }
3303    for option in &options.server_options {
3304        validate_protocol_v2_capability_value(option)?;
3305        capabilities.push(Capability {
3306            name: "server-option".into(),
3307            value: Some(option.clone()),
3308        });
3309    }
3310    for capability in &options.extra {
3311        if matches!(
3312            capability.name.as_str(),
3313            "agent" | "object-format" | "server-option"
3314        ) {
3315            return Err(GitError::InvalidFormat(format!(
3316                "protocol v2 extra capability duplicates known capability {}",
3317                capability.name
3318            )));
3319        }
3320        encode_protocol_v2_capability(capability)?;
3321        capabilities.push(capability.clone());
3322    }
3323    Ok(capabilities)
3324}
3325
3326pub fn parse_protocol_v2_ls_refs_features(
3327    capabilities: &[Capability],
3328) -> Result<Option<ProtocolV2LsRefsFeatures>> {
3329    let mut ls_refs = None;
3330    for capability in capabilities {
3331        if capability.name != "ls-refs" {
3332            continue;
3333        }
3334        if ls_refs.is_some() {
3335            return Err(GitError::InvalidFormat(
3336                "protocol v2 has duplicate ls-refs capabilities".into(),
3337            ));
3338        }
3339        ls_refs = Some(parse_protocol_v2_ls_refs_feature_value(
3340            capability.value.as_deref(),
3341        )?);
3342    }
3343    Ok(ls_refs)
3344}
3345
3346pub fn encode_protocol_v2_ls_refs_capability(
3347    features: &ProtocolV2LsRefsFeatures,
3348) -> Result<Capability> {
3349    let mut values = Vec::new();
3350    if features.unborn {
3351        values.push("unborn".to_string());
3352    }
3353    for feature in &features.unknown {
3354        validate_protocol_v2_token("ls-refs feature", feature)?;
3355        if feature == "unborn" {
3356            return Err(GitError::InvalidFormat(
3357                "ls-refs unknown features must not duplicate known feature unborn".into(),
3358            ));
3359        }
3360        values.push(feature.clone());
3361    }
3362    Ok(Capability {
3363        name: "ls-refs".into(),
3364        value: (!values.is_empty()).then(|| values.join(" ")),
3365    })
3366}
3367
3368pub fn validate_protocol_v2_ls_refs_request_features(
3369    features: &ProtocolV2LsRefsFeatures,
3370    request: &ProtocolV2LsRefsRequest,
3371) -> Result<()> {
3372    if request.unborn && !features.unborn {
3373        return Err(GitError::InvalidFormat(
3374            "ls-refs request uses unborn without advertised unborn feature".into(),
3375        ));
3376    }
3377    Ok(())
3378}
3379
3380pub fn validate_protocol_v2_ls_refs_command_request(
3381    handshake: &TransportHandshake,
3382    request: &ProtocolV2CommandRequest,
3383) -> Result<ProtocolV2LsRefsRequest> {
3384    validate_protocol_v2_command_request_capabilities(handshake, request)?;
3385    let ls_refs = ProtocolV2LsRefsRequest::from_command_request(request)?;
3386    let features = parse_protocol_v2_ls_refs_features(&handshake.capabilities)?
3387        .ok_or_else(|| GitError::InvalidFormat("ls-refs command was not advertised".into()))?;
3388    validate_protocol_v2_ls_refs_request_features(&features, &ls_refs)?;
3389    Ok(ls_refs)
3390}
3391
3392pub fn parse_protocol_v2_fetch_features(
3393    capabilities: &[Capability],
3394) -> Result<Option<ProtocolV2FetchFeatures>> {
3395    let mut fetch = None;
3396    for capability in capabilities {
3397        if capability.name != "fetch" {
3398            continue;
3399        }
3400        if fetch.is_some() {
3401            return Err(GitError::InvalidFormat(
3402                "protocol v2 has duplicate fetch capabilities".into(),
3403            ));
3404        }
3405        fetch = Some(parse_protocol_v2_fetch_feature_value(
3406            capability.value.as_deref(),
3407        )?);
3408    }
3409    Ok(fetch)
3410}
3411
3412pub fn encode_protocol_v2_fetch_capability(
3413    features: &ProtocolV2FetchFeatures,
3414) -> Result<Capability> {
3415    let mut values = Vec::new();
3416    if features.shallow {
3417        values.push("shallow".to_string());
3418    }
3419    if features.wait_for_done {
3420        values.push("wait-for-done".to_string());
3421    }
3422    if features.filter {
3423        values.push("filter".to_string());
3424    }
3425    if features.ref_in_want {
3426        values.push("ref-in-want".to_string());
3427    }
3428    if features.sideband_all {
3429        values.push("sideband-all".to_string());
3430    }
3431    if features.packfile_uris {
3432        values.push("packfile-uris".to_string());
3433    }
3434    for feature in &features.unknown {
3435        validate_protocol_v2_token("fetch feature", feature)?;
3436        if matches!(
3437            feature.as_str(),
3438            "shallow"
3439                | "wait-for-done"
3440                | "filter"
3441                | "ref-in-want"
3442                | "sideband-all"
3443                | "packfile-uris"
3444        ) {
3445            return Err(GitError::InvalidFormat(format!(
3446                "fetch unknown features must not duplicate known feature {feature}"
3447            )));
3448        }
3449        values.push(feature.clone());
3450    }
3451    Ok(Capability {
3452        name: "fetch".into(),
3453        value: (!values.is_empty()).then(|| values.join(" ")),
3454    })
3455}
3456
3457pub fn validate_protocol_v2_fetch_request_features(
3458    features: &ProtocolV2FetchFeatures,
3459    request: &ProtocolV2FetchRequest,
3460) -> Result<()> {
3461    if !features.shallow
3462        && (!request.shallow.is_empty()
3463            || request.deepen.is_some()
3464            || request.deepen_since.is_some()
3465            || !request.deepen_not.is_empty()
3466            || request.deepen_relative)
3467    {
3468        return Err(GitError::InvalidFormat(
3469            "fetch request uses shallow/deepen arguments without advertised shallow feature".into(),
3470        ));
3471    }
3472    if !features.filter && request.filter.is_some() {
3473        return Err(GitError::InvalidFormat(
3474            "fetch request uses filter without advertised filter feature".into(),
3475        ));
3476    }
3477    if !features.ref_in_want && !request.want_refs.is_empty() {
3478        return Err(GitError::InvalidFormat(
3479            "fetch request uses want-ref without advertised ref-in-want feature".into(),
3480        ));
3481    }
3482    if !features.sideband_all && request.sideband_all {
3483        return Err(GitError::InvalidFormat(
3484            "fetch request uses sideband-all without advertised sideband-all feature".into(),
3485        ));
3486    }
3487    if !features.packfile_uris && request.packfile_uris.is_some() {
3488        return Err(GitError::InvalidFormat(
3489            "fetch request uses packfile-uris without advertised packfile-uris feature".into(),
3490        ));
3491    }
3492    if !features.wait_for_done && request.wait_for_done {
3493        return Err(GitError::InvalidFormat(
3494            "fetch request uses wait-for-done without advertised wait-for-done feature".into(),
3495        ));
3496    }
3497    Ok(())
3498}
3499
3500pub fn validate_protocol_v2_fetch_command_request(
3501    handshake: &TransportHandshake,
3502    format: ObjectFormat,
3503    request: &ProtocolV2CommandRequest,
3504) -> Result<ProtocolV2FetchRequest> {
3505    validate_protocol_v2_command_request_capabilities(handshake, request)?;
3506    let fetch = ProtocolV2FetchRequest::from_command_request(format, request)?;
3507    let features = parse_protocol_v2_fetch_features(&handshake.capabilities)?
3508        .ok_or_else(|| GitError::InvalidFormat("fetch command was not advertised".into()))?;
3509    validate_protocol_v2_fetch_request_features(&features, &fetch)?;
3510    Ok(fetch)
3511}
3512
3513pub fn validate_protocol_v2_object_info_command_request(
3514    handshake: &TransportHandshake,
3515    format: ObjectFormat,
3516    request: &ProtocolV2CommandRequest,
3517) -> Result<ProtocolV2ObjectInfoRequest> {
3518    validate_protocol_v2_command_request_capabilities(handshake, request)?;
3519    let object_info = ProtocolV2ObjectInfoRequest::from_command_request(format, request)?;
3520    protocol_v2_capability(&handshake.capabilities, "object-info")
3521        .ok_or_else(|| GitError::InvalidFormat("object-info command was not advertised".into()))?;
3522    Ok(object_info)
3523}
3524
3525pub fn classify_protocol_v2_command_request(
3526    handshake: &TransportHandshake,
3527    format: ObjectFormat,
3528    request: &ProtocolV2CommandRequest,
3529) -> Result<ProtocolV2Command> {
3530    match request.command.as_str() {
3531        "ls-refs" => validate_protocol_v2_ls_refs_command_request(handshake, request)
3532            .map(ProtocolV2Command::LsRefs),
3533        "fetch" => validate_protocol_v2_fetch_command_request(handshake, format, request)
3534            .map(ProtocolV2Command::Fetch),
3535        "object-info" => {
3536            validate_protocol_v2_object_info_command_request(handshake, format, request)
3537                .map(ProtocolV2Command::ObjectInfo)
3538        }
3539        _ => {
3540            validate_protocol_v2_command_request_capabilities(handshake, request)?;
3541            Ok(ProtocolV2Command::Unknown(request.clone()))
3542        }
3543    }
3544}
3545
3546pub fn classify_protocol_v2_request(
3547    handshake: &TransportHandshake,
3548    format: ObjectFormat,
3549    request: &ProtocolV2Request,
3550) -> Result<ProtocolV2SessionRequest> {
3551    match request {
3552        ProtocolV2Request::Command(command) => {
3553            classify_protocol_v2_command_request(handshake, format, command)
3554                .map(ProtocolV2SessionRequest::Command)
3555        }
3556        ProtocolV2Request::Done => Ok(ProtocolV2SessionRequest::Done),
3557    }
3558}
3559
3560pub fn read_protocol_v2_session_request(
3561    handshake: &TransportHandshake,
3562    format: ObjectFormat,
3563    reader: &mut impl Read,
3564) -> Result<ProtocolV2SessionRequest> {
3565    let request = read_protocol_v2_request(reader)?;
3566    classify_protocol_v2_request(handshake, format, &request)
3567}
3568
3569fn protocol_v2_capability<'a>(
3570    capabilities: &'a [Capability],
3571    name: &str,
3572) -> Option<&'a Capability> {
3573    capabilities
3574        .iter()
3575        .find(|capability| capability.name == name)
3576}
3577
3578fn validate_protocol_v2_object_format_request(
3579    advertised: &Capability,
3580    requested: &Capability,
3581) -> Result<()> {
3582    let Some(advertised) = &advertised.value else {
3583        return Err(GitError::InvalidFormat(
3584            "advertised object-format capability is missing a value".into(),
3585        ));
3586    };
3587    let Some(requested) = &requested.value else {
3588        return Err(GitError::InvalidFormat(
3589            "requested object-format capability is missing a value".into(),
3590        ));
3591    };
3592    if advertised != requested {
3593        return Err(GitError::InvalidFormat(format!(
3594            "requested object-format {requested} does not match advertised {advertised}"
3595        )));
3596    }
3597    Ok(())
3598}
3599
3600fn parse_protocol_v2_ls_refs_feature_value(
3601    value: Option<&str>,
3602) -> Result<ProtocolV2LsRefsFeatures> {
3603    let mut out = ProtocolV2LsRefsFeatures::default();
3604    let Some(value) = value else {
3605        return Ok(out);
3606    };
3607    if value.is_empty() {
3608        return Err(GitError::InvalidFormat(
3609            "protocol v2 ls-refs capability value is empty".into(),
3610        ));
3611    }
3612    for feature in value.split(' ') {
3613        validate_protocol_v2_token("ls-refs feature", feature)?;
3614        match feature {
3615            "unborn" => out.unborn = true,
3616            other => out.unknown.push(other.to_string()),
3617        }
3618    }
3619    Ok(out)
3620}
3621
3622fn parse_protocol_v2_fetch_feature_value(value: Option<&str>) -> Result<ProtocolV2FetchFeatures> {
3623    let mut out = ProtocolV2FetchFeatures::default();
3624    let Some(value) = value else {
3625        return Ok(out);
3626    };
3627    if value.is_empty() {
3628        return Err(GitError::InvalidFormat(
3629            "protocol v2 fetch capability value is empty".into(),
3630        ));
3631    }
3632    for feature in value.split(' ') {
3633        validate_protocol_v2_token("fetch feature", feature)?;
3634        match feature {
3635            "shallow" => out.shallow = true,
3636            "wait-for-done" => out.wait_for_done = true,
3637            "filter" => out.filter = true,
3638            "ref-in-want" => out.ref_in_want = true,
3639            "sideband-all" => out.sideband_all = true,
3640            "packfile-uris" => out.packfile_uris = true,
3641            other => out.unknown.push(other.to_string()),
3642        }
3643    }
3644    Ok(out)
3645}
3646
3647pub fn parse_capabilities(input: &[u8]) -> Result<Vec<Capability>> {
3648    let input = trim_trailing_lf(input);
3649    if input.is_empty() {
3650        return Ok(Vec::new());
3651    }
3652    let text =
3653        std::str::from_utf8(input).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
3654    text.split(' ')
3655        .map(parse_capability_token)
3656        .collect::<Result<Vec<_>>>()
3657}
3658
3659pub fn encode_capabilities(capabilities: &[Capability]) -> Result<Vec<u8>> {
3660    let mut out = Vec::new();
3661    for (idx, capability) in capabilities.iter().enumerate() {
3662        validate_capability_field("capability name", &capability.name)?;
3663        if idx != 0 {
3664            out.push(b' ');
3665        }
3666        out.extend_from_slice(capability.name.as_bytes());
3667        if let Some(value) = &capability.value {
3668            validate_capability_field("capability value", value)?;
3669            out.push(b'=');
3670            out.extend_from_slice(value.as_bytes());
3671        }
3672    }
3673    Ok(out)
3674}
3675
3676pub fn parse_ref_advertisement(format: ObjectFormat, payload: &[u8]) -> Result<RefAdvertisement> {
3677    let payload = trim_trailing_lf(payload);
3678    let (reference, capabilities) = match payload.iter().position(|byte| *byte == 0) {
3679        Some(idx) => (&payload[..idx], parse_capabilities(&payload[idx + 1..])?),
3680        None => (payload, Vec::new()),
3681    };
3682    let text =
3683        std::str::from_utf8(reference).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
3684    let (oid, name) = text
3685        .split_once(' ')
3686        .ok_or_else(|| GitError::InvalidFormat("advertised ref is missing name".into()))?;
3687    if name.is_empty() {
3688        return Err(GitError::InvalidFormat(
3689            "advertised ref name is empty".into(),
3690        ));
3691    }
3692    Ok(RefAdvertisement {
3693        oid: ObjectId::from_hex(format, oid)?,
3694        name: name.to_string(),
3695        capabilities,
3696    })
3697}
3698
3699pub fn encode_ref_advertisement(advertisement: &RefAdvertisement) -> Result<Vec<u8>> {
3700    validate_protocol_v2_token("advertised ref name", &advertisement.name)?;
3701    let mut out = advertisement.oid.to_string().into_bytes();
3702    out.push(b' ');
3703    out.extend_from_slice(advertisement.name.as_bytes());
3704    if !advertisement.capabilities.is_empty() {
3705        out.push(0);
3706        out.extend_from_slice(&encode_capabilities(&advertisement.capabilities)?);
3707    }
3708    out.push(b'\n');
3709    Ok(out)
3710}
3711
3712pub fn parse_ref_advertisements(
3713    format: ObjectFormat,
3714    frames: &[PktLineFrame],
3715) -> Result<Vec<RefAdvertisement>> {
3716    Ok(parse_ref_advertisement_set(format, frames)?.refs)
3717}
3718
3719pub fn parse_ref_advertisement_set(
3720    format: ObjectFormat,
3721    frames: &[PktLineFrame],
3722) -> Result<RefAdvertisementSet> {
3723    let mut set = RefAdvertisementSet {
3724        protocol: ProtocolVersion::V0,
3725        refs: Vec::new(),
3726        shallow: Vec::new(),
3727    };
3728    let mut saw_flush = false;
3729    let mut in_shallow = false;
3730    for (idx, frame) in frames.iter().enumerate() {
3731        match frame {
3732            PktLineFrame::Data(payload) if !saw_flush => {
3733                let trimmed = trim_trailing_lf(payload);
3734                if trimmed == b"version 1" {
3735                    if idx != 0 {
3736                        return Err(GitError::InvalidFormat(
3737                            "advertised ref protocol version must be the first line".into(),
3738                        ));
3739                    }
3740                    set.protocol = ProtocolVersion::V1;
3741                    continue;
3742                }
3743                if trimmed.starts_with(b"version ") {
3744                    return Err(GitError::InvalidFormat(
3745                        "unsupported advertised ref protocol version".into(),
3746                    ));
3747                }
3748                if trimmed.starts_with(b"shallow ") {
3749                    if set.refs.is_empty() {
3750                        return Err(GitError::InvalidFormat(
3751                            "advertised shallow refs must follow advertised refs".into(),
3752                        ));
3753                    }
3754                    let text = parse_protocol_v2_line_text("advertised shallow ref", payload)?;
3755                    set.shallow.push(parse_oid_argument(
3756                        format,
3757                        "advertised shallow ref",
3758                        text,
3759                        "shallow ",
3760                    )?);
3761                    in_shallow = true;
3762                    continue;
3763                }
3764                if in_shallow {
3765                    return Err(GitError::InvalidFormat(
3766                        "advertised refs must not follow shallow refs".into(),
3767                    ));
3768                }
3769                let advertisement = parse_ref_advertisement(format, payload)?;
3770                if !set.refs.is_empty() && !advertisement.capabilities.is_empty() {
3771                    return Err(GitError::InvalidFormat(
3772                        "advertised ref capabilities must appear on the first ref".into(),
3773                    ));
3774                }
3775                set.refs.push(advertisement);
3776            }
3777            PktLineFrame::Data(_) => {
3778                return Err(GitError::InvalidFormat(
3779                    "advertised ref stream has data after flush".into(),
3780                ));
3781            }
3782            PktLineFrame::Flush => {
3783                saw_flush = true;
3784                if idx + 1 != frames.len() {
3785                    return Err(GitError::InvalidFormat(
3786                        "advertised ref stream has frames after flush".into(),
3787                    ));
3788                }
3789            }
3790            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
3791                return Err(GitError::InvalidFormat(
3792                    "advertised ref stream contains a non-flush control packet".into(),
3793                ));
3794            }
3795        }
3796    }
3797    if !saw_flush {
3798        return Err(GitError::InvalidFormat(
3799            "advertised ref stream missing flush".into(),
3800        ));
3801    }
3802    Ok(set)
3803}
3804
3805pub fn encode_ref_advertisements(advertisements: &[RefAdvertisement]) -> Result<Vec<PktLineFrame>> {
3806    encode_ref_advertisement_set(&RefAdvertisementSet {
3807        protocol: ProtocolVersion::V0,
3808        refs: advertisements.to_vec(),
3809        shallow: Vec::new(),
3810    })
3811}
3812
3813pub fn encode_ref_advertisement_set(set: &RefAdvertisementSet) -> Result<Vec<PktLineFrame>> {
3814    let mut frames = Vec::new();
3815    match set.protocol {
3816        ProtocolVersion::V0 => {}
3817        ProtocolVersion::V1 => frames.push(PktLineFrame::data(line_from_str("version 1"))?),
3818        ProtocolVersion::V2 => {
3819            return Err(GitError::InvalidFormat(
3820                "protocol v2 does not use v0/v1 advertised-ref streams".into(),
3821            ));
3822        }
3823    }
3824    if set.refs.is_empty() && !set.shallow.is_empty() {
3825        return Err(GitError::InvalidFormat(
3826            "advertised shallow refs require advertised refs".into(),
3827        ));
3828    }
3829    for (idx, advertisement) in set.refs.iter().enumerate() {
3830        if idx != 0 && !advertisement.capabilities.is_empty() {
3831            return Err(GitError::InvalidFormat(
3832                "advertised ref capabilities must appear on the first ref".into(),
3833            ));
3834        }
3835        frames.push(PktLineFrame::data(encode_ref_advertisement(
3836            advertisement,
3837        )?)?);
3838    }
3839    for oid in &set.shallow {
3840        frames.push(PktLineFrame::data(line_from_str(&format!(
3841            "shallow {oid}"
3842        )))?);
3843    }
3844    frames.push(PktLineFrame::Flush);
3845    Ok(frames)
3846}
3847
3848pub fn read_ref_advertisements(
3849    format: ObjectFormat,
3850    reader: &mut impl Read,
3851) -> Result<Vec<RefAdvertisement>> {
3852    let frames = read_pkt_line_frames_until_flush(reader)?;
3853    parse_ref_advertisements(format, &frames)
3854}
3855
3856pub fn read_ref_advertisement_set(
3857    format: ObjectFormat,
3858    reader: &mut impl Read,
3859) -> Result<RefAdvertisementSet> {
3860    let frames = read_pkt_line_frames_until_flush(reader)?;
3861    parse_ref_advertisement_set(format, &frames)
3862}
3863
3864pub fn write_ref_advertisements(
3865    writer: &mut impl Write,
3866    advertisements: &[RefAdvertisement],
3867) -> Result<()> {
3868    write_ref_advertisement_stream(writer, ProtocolVersion::V0, advertisements, &[])
3869}
3870
3871pub fn write_ref_advertisement_set(
3872    writer: &mut impl Write,
3873    set: &RefAdvertisementSet,
3874) -> Result<()> {
3875    write_ref_advertisement_stream(writer, set.protocol, &set.refs, &set.shallow)
3876}
3877
3878fn write_ref_advertisement_stream(
3879    writer: &mut impl Write,
3880    protocol: ProtocolVersion,
3881    refs: &[RefAdvertisement],
3882    shallow: &[ObjectId],
3883) -> Result<()> {
3884    match protocol {
3885        ProtocolVersion::V0 => {}
3886        ProtocolVersion::V1 => write_pkt_line_payload(writer, b"version 1\n")?,
3887        ProtocolVersion::V2 => {
3888            return Err(GitError::InvalidFormat(
3889                "protocol v2 does not use v0/v1 advertised-ref streams".into(),
3890            ));
3891        }
3892    }
3893    if refs.is_empty() && !shallow.is_empty() {
3894        return Err(GitError::InvalidFormat(
3895            "advertised shallow refs require advertised refs".into(),
3896        ));
3897    }
3898    for (idx, advertisement) in refs.iter().enumerate() {
3899        if idx != 0 && !advertisement.capabilities.is_empty() {
3900            return Err(GitError::InvalidFormat(
3901                "advertised ref capabilities must appear on the first ref".into(),
3902            ));
3903        }
3904        write_pkt_line_payload(writer, &encode_ref_advertisement(advertisement)?)?;
3905    }
3906    for oid in shallow {
3907        write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
3908    }
3909    writer.write_all(b"0000")?;
3910    Ok(())
3911}
3912
3913pub fn parse_dumb_http_info_refs(
3914    format: ObjectFormat,
3915    input: &[u8],
3916) -> Result<Vec<DumbHttpRefRecord>> {
3917    if input.is_empty() {
3918        return Ok(Vec::new());
3919    }
3920    input
3921        .split_inclusive(|byte| *byte == b'\n')
3922        .map(|line| parse_dumb_http_info_ref_record(format, line))
3923        .collect()
3924}
3925
3926pub fn encode_dumb_http_info_refs(records: &[DumbHttpRefRecord]) -> Result<Vec<u8>> {
3927    let mut out = Vec::new();
3928    for record in records {
3929        validate_dumb_http_ref_name(&record.name)?;
3930        out.extend_from_slice(record.oid.to_string().as_bytes());
3931        out.push(b'\t');
3932        out.extend_from_slice(record.name.as_bytes());
3933        if record.peeled {
3934            out.extend_from_slice(b"^{}");
3935        }
3936        out.push(b'\n');
3937    }
3938    Ok(out)
3939}
3940
3941pub fn read_dumb_http_info_refs(
3942    format: ObjectFormat,
3943    reader: &mut impl Read,
3944) -> Result<Vec<DumbHttpRefRecord>> {
3945    let mut input = Vec::new();
3946    reader.read_to_end(&mut input)?;
3947    parse_dumb_http_info_refs(format, &input)
3948}
3949
3950pub fn write_dumb_http_info_refs(
3951    writer: &mut impl Write,
3952    records: &[DumbHttpRefRecord],
3953) -> Result<()> {
3954    for record in records {
3955        validate_dumb_http_ref_name(&record.name)?;
3956        writer.write_all(record.oid.to_string().as_bytes())?;
3957        writer.write_all(b"\t")?;
3958        writer.write_all(record.name.as_bytes())?;
3959        if record.peeled {
3960            writer.write_all(b"^{}")?;
3961        }
3962        writer.write_all(b"\n")?;
3963    }
3964    Ok(())
3965}
3966
3967pub fn parse_dumb_http_alternates(input: &[u8]) -> Result<Vec<String>> {
3968    if input.is_empty() {
3969        return Ok(Vec::new());
3970    }
3971    input
3972        .split_inclusive(|byte| *byte == b'\n')
3973        .map(parse_dumb_http_alternate)
3974        .collect()
3975}
3976
3977pub fn encode_dumb_http_alternates(alternates: &[String]) -> Result<Vec<u8>> {
3978    let mut out = Vec::new();
3979    for alternate in alternates {
3980        validate_dumb_http_alternate(alternate)?;
3981        out.extend_from_slice(alternate.as_bytes());
3982        out.push(b'\n');
3983    }
3984    Ok(out)
3985}
3986
3987pub fn read_dumb_http_alternates(reader: &mut impl Read) -> Result<Vec<String>> {
3988    let mut input = Vec::new();
3989    reader.read_to_end(&mut input)?;
3990    parse_dumb_http_alternates(&input)
3991}
3992
3993pub fn write_dumb_http_alternates(writer: &mut impl Write, alternates: &[String]) -> Result<()> {
3994    for alternate in alternates {
3995        validate_dumb_http_alternate(alternate)?;
3996        writer.write_all(alternate.as_bytes())?;
3997        writer.write_all(b"\n")?;
3998    }
3999    Ok(())
4000}
4001
4002pub fn parse_dumb_http_packs(
4003    format: ObjectFormat,
4004    input: &[u8],
4005) -> Result<Vec<DumbHttpPackRecord>> {
4006    if input.is_empty() {
4007        return Ok(Vec::new());
4008    }
4009    input
4010        .split_inclusive(|byte| *byte == b'\n')
4011        .map(|line| parse_dumb_http_pack_record(format, line))
4012        .collect()
4013}
4014
4015pub fn encode_dumb_http_packs(records: &[DumbHttpPackRecord]) -> Result<Vec<u8>> {
4016    let mut out = Vec::new();
4017    for record in records {
4018        out.extend_from_slice(format!("P pack-{}.pack\n", record.hash).as_bytes());
4019    }
4020    Ok(out)
4021}
4022
4023pub fn read_dumb_http_packs(
4024    format: ObjectFormat,
4025    reader: &mut impl Read,
4026) -> Result<Vec<DumbHttpPackRecord>> {
4027    let mut input = Vec::new();
4028    reader.read_to_end(&mut input)?;
4029    parse_dumb_http_packs(format, &input)
4030}
4031
4032pub fn write_dumb_http_packs(
4033    writer: &mut impl Write,
4034    records: &[DumbHttpPackRecord],
4035) -> Result<()> {
4036    for record in records {
4037        writer.write_all(format!("P pack-{}.pack\n", record.hash).as_bytes())?;
4038    }
4039    Ok(())
4040}
4041
4042pub fn parse_upload_pack_request(
4043    format: ObjectFormat,
4044    frames: &[PktLineFrame],
4045) -> Result<Option<UploadPackRequest>> {
4046    if matches!(frames, [PktLineFrame::Flush]) {
4047        return Ok(None);
4048    }
4049
4050    let mut request = UploadPackRequest::default();
4051    let mut in_options = false;
4052    let mut saw_flush = false;
4053    for (idx, frame) in frames.iter().enumerate() {
4054        match frame {
4055            PktLineFrame::Data(payload) if !saw_flush => {
4056                let text = parse_protocol_v2_line_text("upload-pack request line", payload)?;
4057                if let Some(value) = text.strip_prefix("want ") {
4058                    if in_options {
4059                        return Err(GitError::InvalidFormat(
4060                            "upload-pack request has want after options".into(),
4061                        ));
4062                    }
4063                    let (oid, capabilities) = if request.wants.is_empty() {
4064                        value
4065                            .split_once(' ')
4066                            .map_or((value, None), |(oid, caps)| (oid, Some(caps.as_bytes())))
4067                    } else {
4068                        if value.contains(' ') {
4069                            return Err(GitError::InvalidFormat(
4070                                "additional upload-pack want has capabilities".into(),
4071                            ));
4072                        }
4073                        (value, None)
4074                    };
4075                    validate_protocol_v2_token("upload-pack want", oid)?;
4076                    request.wants.push(ObjectId::from_hex(format, oid)?);
4077                    if let Some(capabilities) = capabilities {
4078                        request.capabilities = parse_capabilities(capabilities)?;
4079                    }
4080                    continue;
4081                }
4082
4083                if request.wants.is_empty() {
4084                    return Err(GitError::InvalidFormat(
4085                        "upload-pack request must start with want".into(),
4086                    ));
4087                }
4088                in_options = true;
4089                if text.starts_with("shallow ") {
4090                    request.shallow.push(parse_oid_argument(
4091                        format,
4092                        "upload-pack shallow",
4093                        text,
4094                        "shallow ",
4095                    )?);
4096                } else if text.starts_with("deepen ") {
4097                    if request.deepen.is_some() {
4098                        return Err(GitError::InvalidFormat(
4099                            "upload-pack request has duplicate deepen".into(),
4100                        ));
4101                    }
4102                    request.deepen =
4103                        Some(parse_u32_argument("upload-pack deepen", text, "deepen ")?);
4104                } else if text.starts_with("deepen-since ") {
4105                    if request.deepen_since.is_some() {
4106                        return Err(GitError::InvalidFormat(
4107                            "upload-pack request has duplicate deepen-since".into(),
4108                        ));
4109                    }
4110                    request.deepen_since = Some(parse_u64_argument(
4111                        "upload-pack deepen-since",
4112                        text,
4113                        "deepen-since ",
4114                    )?);
4115                } else if let Some(name) = text.strip_prefix("deepen-not ") {
4116                    validate_protocol_v2_token("upload-pack deepen-not", name)?;
4117                    request.deepen_not.push(name.to_string());
4118                } else if let Some(filter) = text.strip_prefix("filter ") {
4119                    if request.filter.is_some() {
4120                        return Err(GitError::InvalidFormat(
4121                            "upload-pack request has duplicate filter".into(),
4122                        ));
4123                    }
4124                    validate_protocol_v2_token("upload-pack filter", filter)?;
4125                    request.filter = Some(filter.to_string());
4126                } else {
4127                    return Err(GitError::InvalidFormat(format!(
4128                        "unsupported upload-pack request line {text}"
4129                    )));
4130                }
4131            }
4132            PktLineFrame::Data(_) => {
4133                return Err(GitError::InvalidFormat(
4134                    "upload-pack request has data after flush".into(),
4135                ));
4136            }
4137            PktLineFrame::Flush => {
4138                saw_flush = true;
4139                if idx + 1 != frames.len() {
4140                    return Err(GitError::InvalidFormat(
4141                        "upload-pack request has frames after flush".into(),
4142                    ));
4143                }
4144            }
4145            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4146                return Err(GitError::InvalidFormat(
4147                    "upload-pack request contains a non-flush control packet".into(),
4148                ));
4149            }
4150        }
4151    }
4152    if !saw_flush {
4153        return Err(GitError::InvalidFormat(
4154            "upload-pack request missing flush".into(),
4155        ));
4156    }
4157    if request.wants.is_empty() {
4158        return Err(GitError::InvalidFormat(
4159            "upload-pack request missing want".into(),
4160        ));
4161    }
4162    Ok(Some(request))
4163}
4164
4165pub fn encode_upload_pack_request(
4166    request: Option<&UploadPackRequest>,
4167) -> Result<Vec<PktLineFrame>> {
4168    let Some(request) = request else {
4169        return Ok(vec![PktLineFrame::Flush]);
4170    };
4171    if request.wants.is_empty() {
4172        return Err(GitError::InvalidFormat(
4173            "upload-pack request missing want".into(),
4174        ));
4175    }
4176
4177    let mut frames = Vec::new();
4178    for (idx, oid) in request.wants.iter().enumerate() {
4179        let mut line = format!("want {oid}");
4180        if idx == 0 && !request.capabilities.is_empty() {
4181            line.push(' ');
4182            line.push_str(
4183                &String::from_utf8(encode_capabilities(&request.capabilities)?)
4184                    .map_err(|err| GitError::InvalidFormat(err.to_string()))?,
4185            );
4186        }
4187        frames.push(PktLineFrame::data(line_from_str(&line))?);
4188    }
4189    for oid in &request.shallow {
4190        frames.push(PktLineFrame::data(line_from_str(&format!(
4191            "shallow {oid}"
4192        )))?);
4193    }
4194    if let Some(deepen) = request.deepen {
4195        if deepen == 0 {
4196            return Err(GitError::InvalidFormat(
4197                "upload-pack deepen must be positive".into(),
4198            ));
4199        }
4200        frames.push(PktLineFrame::data(line_from_str(&format!(
4201            "deepen {deepen}"
4202        )))?);
4203    }
4204    if let Some(deepen_since) = request.deepen_since {
4205        frames.push(PktLineFrame::data(line_from_str(&format!(
4206            "deepen-since {deepen_since}"
4207        )))?);
4208    }
4209    for name in &request.deepen_not {
4210        validate_protocol_v2_token("upload-pack deepen-not", name)?;
4211        frames.push(PktLineFrame::data(line_from_str(&format!(
4212            "deepen-not {name}"
4213        )))?);
4214    }
4215    if let Some(filter) = &request.filter {
4216        validate_protocol_v2_token("upload-pack filter", filter)?;
4217        frames.push(PktLineFrame::data(line_from_str(&format!(
4218            "filter {filter}"
4219        )))?);
4220    }
4221    frames.push(PktLineFrame::Flush);
4222    Ok(frames)
4223}
4224
4225pub fn read_upload_pack_request(
4226    format: ObjectFormat,
4227    reader: &mut impl Read,
4228) -> Result<Option<UploadPackRequest>> {
4229    let frames = read_pkt_line_frames_until_flush(reader)?;
4230    parse_upload_pack_request(format, &frames)
4231}
4232
4233pub fn write_upload_pack_request(
4234    writer: &mut impl Write,
4235    request: Option<&UploadPackRequest>,
4236) -> Result<()> {
4237    let Some(request) = request else {
4238        writer.write_all(b"0000")?;
4239        return Ok(());
4240    };
4241    if request.wants.is_empty() {
4242        return Err(GitError::InvalidFormat(
4243            "upload-pack request missing want".into(),
4244        ));
4245    }
4246
4247    for (idx, oid) in request.wants.iter().enumerate() {
4248        let mut line = format!("want {oid}");
4249        if idx == 0 && !request.capabilities.is_empty() {
4250            line.push(' ');
4251            line.push_str(
4252                &String::from_utf8(encode_capabilities(&request.capabilities)?)
4253                    .map_err(|err| GitError::InvalidFormat(err.to_string()))?,
4254            );
4255        }
4256        write_pkt_line_payload(writer, &line_from_str(&line))?;
4257    }
4258    for oid in &request.shallow {
4259        write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
4260    }
4261    if let Some(deepen) = request.deepen {
4262        if deepen == 0 {
4263            return Err(GitError::InvalidFormat(
4264                "upload-pack deepen must be positive".into(),
4265            ));
4266        }
4267        write_pkt_line_payload(writer, &line_from_str(&format!("deepen {deepen}")))?;
4268    }
4269    if let Some(deepen_since) = request.deepen_since {
4270        write_pkt_line_payload(
4271            writer,
4272            &line_from_str(&format!("deepen-since {deepen_since}")),
4273        )?;
4274    }
4275    for name in &request.deepen_not {
4276        validate_protocol_v2_token("upload-pack deepen-not", name)?;
4277        write_pkt_line_payload(writer, &line_from_str(&format!("deepen-not {name}")))?;
4278    }
4279    if let Some(filter) = &request.filter {
4280        validate_protocol_v2_token("upload-pack filter", filter)?;
4281        write_pkt_line_payload(writer, &line_from_str(&format!("filter {filter}")))?;
4282    }
4283    writer.write_all(b"0000")?;
4284    Ok(())
4285}
4286
4287pub fn parse_upload_pack_features(capabilities: &[Capability]) -> Result<UploadPackFeatures> {
4288    let mut features = UploadPackFeatures::default();
4289    for capability in capabilities {
4290        match capability.name.as_str() {
4291            "multi_ack" => set_upload_pack_flag(&mut features.multi_ack, capability)?,
4292            "multi_ack_detailed" => {
4293                set_upload_pack_flag(&mut features.multi_ack_detailed, capability)?
4294            }
4295            "no-done" => set_upload_pack_flag(&mut features.no_done, capability)?,
4296            "thin-pack" => set_upload_pack_flag(&mut features.thin_pack, capability)?,
4297            "side-band" => set_upload_pack_flag(&mut features.side_band, capability)?,
4298            "side-band-64k" => set_upload_pack_flag(&mut features.side_band_64k, capability)?,
4299            "ofs-delta" => set_upload_pack_flag(&mut features.ofs_delta, capability)?,
4300            "shallow" => set_upload_pack_flag(&mut features.shallow, capability)?,
4301            "deepen-since" => set_upload_pack_flag(&mut features.deepen_since, capability)?,
4302            "deepen-not" => set_upload_pack_flag(&mut features.deepen_not, capability)?,
4303            "include-tag" => set_upload_pack_flag(&mut features.include_tag, capability)?,
4304            "no-progress" => set_upload_pack_flag(&mut features.no_progress, capability)?,
4305            "allow-tip-sha1-in-want" => {
4306                set_upload_pack_flag(&mut features.allow_tip_sha1_in_want, capability)?
4307            }
4308            "allow-reachable-sha1-in-want" => {
4309                set_upload_pack_flag(&mut features.allow_reachable_sha1_in_want, capability)?
4310            }
4311            "filter" => set_upload_pack_flag(&mut features.filter, capability)?,
4312            "agent" => {
4313                let Some(agent) = &capability.value else {
4314                    return Err(GitError::InvalidFormat(
4315                        "upload-pack agent capability is missing value".into(),
4316                    ));
4317                };
4318                if features.agent.is_some() {
4319                    return Err(GitError::InvalidFormat(
4320                        "upload-pack has duplicate agent capability".into(),
4321                    ));
4322                }
4323                validate_capability_field("upload-pack agent", agent)?;
4324                features.agent = Some(agent.clone());
4325            }
4326            "object-format" => {
4327                let Some(format) = &capability.value else {
4328                    return Err(GitError::InvalidFormat(
4329                        "upload-pack object-format capability is missing value".into(),
4330                    ));
4331                };
4332                if features.object_format.is_some() {
4333                    return Err(GitError::InvalidFormat(
4334                        "upload-pack has duplicate object-format capability".into(),
4335                    ));
4336                }
4337                validate_capability_field("upload-pack object-format", format)?;
4338                features.object_format = Some(format.parse()?);
4339            }
4340            "symref" => {
4341                let Some(symref) = &capability.value else {
4342                    continue;
4343                };
4344                validate_capability_field("upload-pack symref", symref)?;
4345                features.symrefs.push(symref.clone());
4346            }
4347            _ => {
4348                encode_capabilities(std::slice::from_ref(capability))?;
4349                if features
4350                    .unknown
4351                    .iter()
4352                    .any(|known| known.name == capability.name)
4353                {
4354                    return Err(GitError::InvalidFormat(format!(
4355                        "upload-pack has duplicate {} capability",
4356                        capability.name
4357                    )));
4358                }
4359                features.unknown.push(capability.clone());
4360            }
4361        }
4362    }
4363    Ok(features)
4364}
4365
4366pub fn encode_upload_pack_features(features: &UploadPackFeatures) -> Result<Vec<Capability>> {
4367    let mut capabilities = Vec::new();
4368    push_upload_pack_flag(&mut capabilities, "multi_ack", features.multi_ack);
4369    push_upload_pack_flag(
4370        &mut capabilities,
4371        "multi_ack_detailed",
4372        features.multi_ack_detailed,
4373    );
4374    push_upload_pack_flag(&mut capabilities, "no-done", features.no_done);
4375    push_upload_pack_flag(&mut capabilities, "thin-pack", features.thin_pack);
4376    push_upload_pack_flag(&mut capabilities, "side-band", features.side_band);
4377    push_upload_pack_flag(&mut capabilities, "side-band-64k", features.side_band_64k);
4378    push_upload_pack_flag(&mut capabilities, "ofs-delta", features.ofs_delta);
4379    push_upload_pack_flag(&mut capabilities, "shallow", features.shallow);
4380    push_upload_pack_flag(&mut capabilities, "deepen-since", features.deepen_since);
4381    push_upload_pack_flag(&mut capabilities, "deepen-not", features.deepen_not);
4382    push_upload_pack_flag(&mut capabilities, "include-tag", features.include_tag);
4383    push_upload_pack_flag(&mut capabilities, "no-progress", features.no_progress);
4384    push_upload_pack_flag(
4385        &mut capabilities,
4386        "allow-tip-sha1-in-want",
4387        features.allow_tip_sha1_in_want,
4388    );
4389    push_upload_pack_flag(
4390        &mut capabilities,
4391        "allow-reachable-sha1-in-want",
4392        features.allow_reachable_sha1_in_want,
4393    );
4394    push_upload_pack_flag(&mut capabilities, "filter", features.filter);
4395    if let Some(agent) = &features.agent {
4396        validate_capability_field("upload-pack agent", agent)?;
4397        capabilities.push(Capability {
4398            name: "agent".into(),
4399            value: Some(agent.clone()),
4400        });
4401    }
4402    if let Some(format) = features.object_format {
4403        capabilities.push(Capability {
4404            name: "object-format".into(),
4405            value: Some(format.name().into()),
4406        });
4407    }
4408    for symref in &features.symrefs {
4409        validate_capability_field("upload-pack symref", symref)?;
4410        capabilities.push(Capability {
4411            name: "symref".into(),
4412            value: Some(symref.clone()),
4413        });
4414    }
4415    for capability in &features.unknown {
4416        if is_known_upload_pack_capability(&capability.name) {
4417            return Err(GitError::InvalidFormat(format!(
4418                "upload-pack unknown capability duplicates known capability {}",
4419                capability.name
4420            )));
4421        }
4422        encode_capabilities(std::slice::from_ref(capability))?;
4423        capabilities.push(capability.clone());
4424    }
4425    Ok(capabilities)
4426}
4427
4428pub fn validate_upload_pack_request_features(
4429    features: &UploadPackFeatures,
4430    request: &UploadPackRequest,
4431) -> Result<()> {
4432    for capability in &request.capabilities {
4433        if is_upload_pack_flag_capability(&capability.name) {
4434            reject_capability_value("upload-pack request capability", capability)?;
4435        }
4436        match capability.name.as_str() {
4437            "multi_ack" if !features.multi_ack => {
4438                return Err(GitError::InvalidFormat(
4439                    "upload-pack request uses multi_ack without advertised capability".into(),
4440                ));
4441            }
4442            "multi_ack_detailed" if !features.multi_ack_detailed => {
4443                return Err(GitError::InvalidFormat(
4444                    "upload-pack request uses multi_ack_detailed without advertised capability"
4445                        .into(),
4446                ));
4447            }
4448            "no-done" if !features.no_done => {
4449                return Err(GitError::InvalidFormat(
4450                    "upload-pack request uses no-done without advertised capability".into(),
4451                ));
4452            }
4453            "thin-pack" if !features.thin_pack => {
4454                return Err(GitError::InvalidFormat(
4455                    "upload-pack request uses thin-pack without advertised capability".into(),
4456                ));
4457            }
4458            "side-band" if !features.side_band => {
4459                return Err(GitError::InvalidFormat(
4460                    "upload-pack request uses side-band without advertised capability".into(),
4461                ));
4462            }
4463            "side-band-64k" if !features.side_band_64k => {
4464                return Err(GitError::InvalidFormat(
4465                    "upload-pack request uses side-band-64k without advertised capability".into(),
4466                ));
4467            }
4468            "ofs-delta" if !features.ofs_delta => {
4469                return Err(GitError::InvalidFormat(
4470                    "upload-pack request uses ofs-delta without advertised capability".into(),
4471                ));
4472            }
4473            "include-tag" if !features.include_tag => {
4474                return Err(GitError::InvalidFormat(
4475                    "upload-pack request uses include-tag without advertised capability".into(),
4476                ));
4477            }
4478            "no-progress" if !features.no_progress => {
4479                return Err(GitError::InvalidFormat(
4480                    "upload-pack request uses no-progress without advertised capability".into(),
4481                ));
4482            }
4483            "allow-tip-sha1-in-want" if !features.allow_tip_sha1_in_want => {
4484                return Err(GitError::InvalidFormat(
4485                    "upload-pack request uses allow-tip-sha1-in-want without advertised capability"
4486                        .into(),
4487                ));
4488            }
4489            "allow-reachable-sha1-in-want" if !features.allow_reachable_sha1_in_want => {
4490                return Err(GitError::InvalidFormat(
4491                    "upload-pack request uses allow-reachable-sha1-in-want without advertised capability"
4492                        .into(),
4493                ));
4494            }
4495            "filter" if !features.filter => {
4496                return Err(GitError::InvalidFormat(
4497                    "upload-pack request uses filter capability without advertised capability"
4498                        .into(),
4499                ));
4500            }
4501            "agent" => {
4502                let Some(agent) = &capability.value else {
4503                    return Err(GitError::InvalidFormat(
4504                        "upload-pack request agent capability is missing value".into(),
4505                    ));
4506                };
4507                validate_capability_field("upload-pack request agent", agent)?;
4508            }
4509            "object-format" => {
4510                let Some(format) = &capability.value else {
4511                    return Err(GitError::InvalidFormat(
4512                        "upload-pack request object-format capability is missing value".into(),
4513                    ));
4514                };
4515                let requested_format: ObjectFormat = format.parse()?;
4516                if features.object_format != Some(requested_format) {
4517                    return Err(GitError::InvalidFormat(
4518                        "upload-pack request object-format was not advertised".into(),
4519                    ));
4520                }
4521            }
4522            name if is_known_upload_pack_capability(name) => {}
4523            _ => {
4524                if !features
4525                    .unknown
4526                    .iter()
4527                    .any(|advertised| advertised.name == capability.name)
4528                {
4529                    return Err(GitError::InvalidFormat(format!(
4530                        "upload-pack request uses unadvertised capability {}",
4531                        capability.name
4532                    )));
4533                }
4534            }
4535        }
4536    }
4537
4538    let sideband = request
4539        .capabilities
4540        .iter()
4541        .any(|capability| capability.name == "side-band");
4542    let sideband_64k = request
4543        .capabilities
4544        .iter()
4545        .any(|capability| capability.name == "side-band-64k");
4546    if sideband && sideband_64k {
4547        return Err(GitError::InvalidFormat(
4548            "upload-pack request must not request both side-band and side-band-64k".into(),
4549        ));
4550    }
4551
4552    if !features.shallow && (!request.shallow.is_empty() || request.deepen.is_some()) {
4553        return Err(GitError::InvalidFormat(
4554            "upload-pack request uses shallow/deepen without advertised shallow capability".into(),
4555        ));
4556    }
4557    if !features.deepen_since && request.deepen_since.is_some() {
4558        return Err(GitError::InvalidFormat(
4559            "upload-pack request uses deepen-since without advertised capability".into(),
4560        ));
4561    }
4562    if !features.deepen_not && !request.deepen_not.is_empty() {
4563        return Err(GitError::InvalidFormat(
4564            "upload-pack request uses deepen-not without advertised capability".into(),
4565        ));
4566    }
4567    if !features.filter && request.filter.is_some() {
4568        return Err(GitError::InvalidFormat(
4569            "upload-pack request uses filter without advertised capability".into(),
4570        ));
4571    }
4572    Ok(())
4573}
4574
4575pub fn build_upload_pack_raw_packfile_response<C, B>(
4576    features: &UploadPackFeatures,
4577    request: UploadPackRequest,
4578    haves: impl IntoIterator<Item = ObjectId>,
4579    mut contains_object: C,
4580    mut build_pack: B,
4581) -> Result<UploadPackRawPackfileResponse>
4582where
4583    C: FnMut(&ObjectId) -> Result<bool>,
4584    B: FnMut(Vec<ObjectId>, Vec<ObjectId>) -> Result<Option<Vec<u8>>>,
4585{
4586    validate_upload_pack_request_features(features, &request)?;
4587    for want in &request.wants {
4588        if !contains_object(want)? {
4589            return Err(GitError::InvalidObject(format!(
4590                "upload-pack requested missing object {want}"
4591            )));
4592        }
4593    }
4594    let known_haves = haves
4595        .into_iter()
4596        .filter_map(|oid| match contains_object(&oid) {
4597            Ok(true) => Some(Ok(oid)),
4598            Ok(false) => None,
4599            Err(err) => Some(Err(err)),
4600        })
4601        .collect::<Result<Vec<_>>>()?;
4602    let packfile = build_pack(request.wants, known_haves)?
4603        .ok_or_else(|| GitError::InvalidObject("upload-pack request produced empty pack".into()))?;
4604    Ok(UploadPackRawPackfileResponse {
4605        acknowledgments: vec![UploadPackAcknowledgment::Nak],
4606        packfile,
4607    })
4608}
4609
4610pub fn parse_upload_pack_shallow_update(
4611    format: ObjectFormat,
4612    frames: &[PktLineFrame],
4613) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
4614    let mut entries = Vec::new();
4615    let mut saw_flush = false;
4616    for (idx, frame) in frames.iter().enumerate() {
4617        match frame {
4618            PktLineFrame::Data(payload) if !saw_flush => {
4619                entries.push(parse_fetch_shallow_info(format, payload)?);
4620            }
4621            PktLineFrame::Data(_) => {
4622                return Err(GitError::InvalidFormat(
4623                    "upload-pack shallow update has data after flush".into(),
4624                ));
4625            }
4626            PktLineFrame::Flush => {
4627                saw_flush = true;
4628                if idx + 1 != frames.len() {
4629                    return Err(GitError::InvalidFormat(
4630                        "upload-pack shallow update has frames after flush".into(),
4631                    ));
4632                }
4633            }
4634            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4635                return Err(GitError::InvalidFormat(
4636                    "upload-pack shallow update contains a non-flush control packet".into(),
4637                ));
4638            }
4639        }
4640    }
4641    if !saw_flush {
4642        return Err(GitError::InvalidFormat(
4643            "upload-pack shallow update missing flush".into(),
4644        ));
4645    }
4646    Ok(entries)
4647}
4648
4649pub fn encode_upload_pack_shallow_update(
4650    entries: &[ProtocolV2FetchShallowInfo],
4651) -> Result<Vec<PktLineFrame>> {
4652    let mut frames = Vec::new();
4653    for entry in entries {
4654        let line = match entry {
4655            ProtocolV2FetchShallowInfo::Shallow(oid) => format!("shallow {oid}"),
4656            ProtocolV2FetchShallowInfo::Unshallow(oid) => format!("unshallow {oid}"),
4657        };
4658        frames.push(PktLineFrame::data(line_from_str(&line))?);
4659    }
4660    frames.push(PktLineFrame::Flush);
4661    Ok(frames)
4662}
4663
4664pub fn read_upload_pack_shallow_update(
4665    format: ObjectFormat,
4666    reader: &mut impl Read,
4667) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
4668    let frames = read_pkt_line_frames_until_flush(reader)?;
4669    parse_upload_pack_shallow_update(format, &frames)
4670}
4671
4672pub fn write_upload_pack_shallow_update(
4673    writer: &mut impl Write,
4674    entries: &[ProtocolV2FetchShallowInfo],
4675) -> Result<()> {
4676    for entry in entries {
4677        let line = match entry {
4678            ProtocolV2FetchShallowInfo::Shallow(oid) => format!("shallow {oid}"),
4679            ProtocolV2FetchShallowInfo::Unshallow(oid) => format!("unshallow {oid}"),
4680        };
4681        write_pkt_line_payload(writer, &line_from_str(&line))?;
4682    }
4683    writer.write_all(b"0000")?;
4684    Ok(())
4685}
4686
4687pub fn parse_upload_pack_negotiation_request(
4688    format: ObjectFormat,
4689    frames: &[PktLineFrame],
4690) -> Result<UploadPackNegotiationRequest> {
4691    let mut request = UploadPackNegotiationRequest::default();
4692    let mut terminated = false;
4693    for (idx, frame) in frames.iter().enumerate() {
4694        match frame {
4695            PktLineFrame::Data(payload) if !terminated => {
4696                let text = parse_protocol_v2_line_text("upload-pack negotiation line", payload)?;
4697                if text == "done" {
4698                    request.done = true;
4699                    terminated = true;
4700                    if idx + 1 != frames.len() {
4701                        return Err(GitError::InvalidFormat(
4702                            "upload-pack negotiation has frames after done".into(),
4703                        ));
4704                    }
4705                } else if text.starts_with("have ") {
4706                    request.haves.push(parse_oid_argument(
4707                        format,
4708                        "upload-pack have",
4709                        text,
4710                        "have ",
4711                    )?);
4712                } else {
4713                    return Err(GitError::InvalidFormat(format!(
4714                        "unsupported upload-pack negotiation line {text}"
4715                    )));
4716                }
4717            }
4718            PktLineFrame::Data(_) => {
4719                return Err(GitError::InvalidFormat(
4720                    "upload-pack negotiation has data after terminator".into(),
4721                ));
4722            }
4723            PktLineFrame::Flush => {
4724                terminated = true;
4725                if idx + 1 != frames.len() {
4726                    return Err(GitError::InvalidFormat(
4727                        "upload-pack negotiation has frames after flush".into(),
4728                    ));
4729                }
4730            }
4731            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4732                return Err(GitError::InvalidFormat(
4733                    "upload-pack negotiation contains a non-flush control packet".into(),
4734                ));
4735            }
4736        }
4737    }
4738    if !terminated {
4739        return Err(GitError::InvalidFormat(
4740            "upload-pack negotiation missing terminator".into(),
4741        ));
4742    }
4743    Ok(request)
4744}
4745
4746pub fn encode_upload_pack_negotiation_request(
4747    request: &UploadPackNegotiationRequest,
4748) -> Result<Vec<PktLineFrame>> {
4749    let mut frames = Vec::new();
4750    for oid in &request.haves {
4751        frames.push(PktLineFrame::data(line_from_str(&format!("have {oid}")))?);
4752    }
4753    if request.done {
4754        frames.push(PktLineFrame::data(line_from_str("done"))?);
4755    } else {
4756        frames.push(PktLineFrame::Flush);
4757    }
4758    Ok(frames)
4759}
4760
4761pub fn read_upload_pack_negotiation_request(
4762    format: ObjectFormat,
4763    reader: &mut impl Read,
4764) -> Result<UploadPackNegotiationRequest> {
4765    let mut frames = Vec::new();
4766    loop {
4767        let Some(frame) = read_pkt_line_frame(reader)? else {
4768            return Err(GitError::InvalidFormat(
4769                "pkt-line stream ended before upload-pack negotiation terminator".into(),
4770            ));
4771        };
4772        let done = match &frame {
4773            PktLineFrame::Flush => true,
4774            PktLineFrame::Data(payload) => trim_trailing_lf(payload) == b"done",
4775            _ => false,
4776        };
4777        frames.push(frame);
4778        if done {
4779            return parse_upload_pack_negotiation_request(format, &frames);
4780        }
4781    }
4782}
4783
4784pub fn write_upload_pack_negotiation_request(
4785    writer: &mut impl Write,
4786    request: &UploadPackNegotiationRequest,
4787) -> Result<()> {
4788    for oid in &request.haves {
4789        write_pkt_line_payload(writer, &line_from_str(&format!("have {oid}")))?;
4790    }
4791    if request.done {
4792        write_pkt_line_payload(writer, b"done\n")?;
4793    } else {
4794        writer.write_all(b"0000")?;
4795    }
4796    Ok(())
4797}
4798
4799pub fn parse_upload_pack_acknowledgment(
4800    format: ObjectFormat,
4801    payload: &[u8],
4802) -> Result<UploadPackAcknowledgment> {
4803    let text = parse_protocol_v2_line_text("upload-pack acknowledgment", payload)?;
4804    if text == "NAK" {
4805        return Ok(UploadPackAcknowledgment::Nak);
4806    }
4807    let Some(rest) = text.strip_prefix("ACK ") else {
4808        return Err(GitError::InvalidFormat(format!(
4809            "unsupported upload-pack acknowledgment {text}"
4810        )));
4811    };
4812    let mut fields = rest.split(' ');
4813    let oid = fields
4814        .next()
4815        .ok_or_else(|| GitError::InvalidFormat("upload-pack ACK missing object id".into()))?;
4816    validate_protocol_v2_token("upload-pack ACK", oid)?;
4817    let status = match fields.next() {
4818        None => None,
4819        Some("continue") => Some(UploadPackAckStatus::Continue),
4820        Some("common") => Some(UploadPackAckStatus::Common),
4821        Some("ready") => Some(UploadPackAckStatus::Ready),
4822        Some(other) => {
4823            return Err(GitError::InvalidFormat(format!(
4824                "unsupported upload-pack ACK status {other}"
4825            )));
4826        }
4827    };
4828    if fields.next().is_some() {
4829        return Err(GitError::InvalidFormat(
4830            "upload-pack ACK has too many fields".into(),
4831        ));
4832    }
4833    Ok(UploadPackAcknowledgment::Ack {
4834        oid: ObjectId::from_hex(format, oid)?,
4835        status,
4836    })
4837}
4838
4839pub fn encode_upload_pack_acknowledgment(
4840    acknowledgment: &UploadPackAcknowledgment,
4841) -> Result<Vec<u8>> {
4842    let line = match acknowledgment {
4843        UploadPackAcknowledgment::Nak => "NAK".to_string(),
4844        UploadPackAcknowledgment::Ack { oid, status } => {
4845            let mut line = format!("ACK {oid}");
4846            if let Some(status) = status {
4847                line.push(' ');
4848                line.push_str(match status {
4849                    UploadPackAckStatus::Continue => "continue",
4850                    UploadPackAckStatus::Common => "common",
4851                    UploadPackAckStatus::Ready => "ready",
4852                });
4853            }
4854            line
4855        }
4856    };
4857    Ok(line_from_str(&line))
4858}
4859
4860pub fn read_upload_pack_acknowledgment(
4861    format: ObjectFormat,
4862    reader: &mut impl Read,
4863) -> Result<UploadPackAcknowledgment> {
4864    let Some(frame) = read_pkt_line_frame(reader)? else {
4865        return Err(GitError::InvalidFormat(
4866            "pkt-line stream ended before upload-pack acknowledgment".into(),
4867        ));
4868    };
4869    match frame {
4870        PktLineFrame::Data(payload) => parse_upload_pack_acknowledgment(format, &payload),
4871        _ => Err(GitError::InvalidFormat(
4872            "upload-pack acknowledgment must be a data packet".into(),
4873        )),
4874    }
4875}
4876
4877pub fn write_upload_pack_acknowledgment(
4878    writer: &mut impl Write,
4879    acknowledgment: &UploadPackAcknowledgment,
4880) -> Result<()> {
4881    write_pkt_line_payload(writer, &encode_upload_pack_acknowledgment(acknowledgment)?)
4882}
4883
4884pub fn parse_upload_pack_packfile_response(
4885    format: ObjectFormat,
4886    frames: &[PktLineFrame],
4887) -> Result<UploadPackPackfileResponse> {
4888    let mut response = UploadPackPackfileResponse::default();
4889    let mut in_sideband = false;
4890    let mut saw_flush = false;
4891    for (idx, frame) in frames.iter().enumerate() {
4892        match frame {
4893            PktLineFrame::Data(payload) if !saw_flush => {
4894                if !in_sideband
4895                    && (trim_trailing_lf(payload) == b"NAK" || payload.starts_with(b"ACK "))
4896                {
4897                    response
4898                        .acknowledgments
4899                        .push(parse_upload_pack_acknowledgment(format, payload)?);
4900                    continue;
4901                }
4902                in_sideband = true;
4903                response.sideband.push(parse_sideband_packet(payload)?);
4904            }
4905            PktLineFrame::Data(_) => {
4906                return Err(GitError::InvalidFormat(
4907                    "upload-pack packfile response has data after flush".into(),
4908                ));
4909            }
4910            PktLineFrame::Flush => {
4911                saw_flush = true;
4912                if idx + 1 != frames.len() {
4913                    return Err(GitError::InvalidFormat(
4914                        "upload-pack packfile response has frames after flush".into(),
4915                    ));
4916                }
4917            }
4918            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
4919                return Err(GitError::InvalidFormat(
4920                    "upload-pack packfile response contains a non-flush control packet".into(),
4921                ));
4922            }
4923        }
4924    }
4925    if !saw_flush {
4926        return Err(GitError::InvalidFormat(
4927            "upload-pack packfile response missing flush".into(),
4928        ));
4929    }
4930    Ok(response)
4931}
4932
4933pub fn encode_upload_pack_packfile_response(
4934    response: &UploadPackPackfileResponse,
4935) -> Result<Vec<PktLineFrame>> {
4936    let mut frames = Vec::new();
4937    for acknowledgment in &response.acknowledgments {
4938        frames.push(PktLineFrame::data(encode_upload_pack_acknowledgment(
4939            acknowledgment,
4940        )?)?);
4941    }
4942    for packet in &response.sideband {
4943        frames.push(PktLineFrame::data(encode_sideband_packet(packet)?)?);
4944    }
4945    frames.push(PktLineFrame::Flush);
4946    Ok(frames)
4947}
4948
4949pub fn read_upload_pack_packfile_response(
4950    format: ObjectFormat,
4951    reader: &mut impl Read,
4952) -> Result<UploadPackPackfileResponse> {
4953    let frames = read_pkt_line_frames_until_flush(reader)?;
4954    parse_upload_pack_packfile_response(format, &frames)
4955}
4956
4957pub fn write_upload_pack_packfile_response(
4958    writer: &mut impl Write,
4959    response: &UploadPackPackfileResponse,
4960) -> Result<()> {
4961    for acknowledgment in &response.acknowledgments {
4962        write_upload_pack_acknowledgment(writer, acknowledgment)?;
4963    }
4964    for packet in &response.sideband {
4965        write_sideband_packet(writer, packet)?;
4966    }
4967    writer.write_all(b"0000")?;
4968    Ok(())
4969}
4970
4971pub fn demux_upload_pack_packfile_response(
4972    response: &UploadPackPackfileResponse,
4973) -> Result<SideBandDemux> {
4974    demux_sideband_packets(&response.sideband)
4975}
4976
4977pub fn parse_upload_pack_raw_packfile_response(
4978    format: ObjectFormat,
4979    input: &[u8],
4980) -> Result<UploadPackRawPackfileResponse> {
4981    let mut response = UploadPackRawPackfileResponse::default();
4982    let mut offset = 0usize;
4983    while offset < input.len() {
4984        match PktLineFrame::parse(&input[offset..]) {
4985            Ok((PktLineFrame::Data(payload), consumed)) => {
4986                let trimmed = trim_trailing_lf(&payload);
4987                if trimmed == b"NAK" || trimmed.starts_with(b"ACK ") {
4988                    response
4989                        .acknowledgments
4990                        .push(parse_upload_pack_acknowledgment(format, &payload)?);
4991                    offset += consumed;
4992                    continue;
4993                }
4994                return Err(GitError::InvalidFormat(
4995                    "upload-pack raw packfile response has non-ack pkt-line before packfile".into(),
4996                ));
4997            }
4998            Ok((PktLineFrame::Flush | PktLineFrame::Delimiter | PktLineFrame::ResponseEnd, _)) => {
4999                return Err(GitError::InvalidFormat(
5000                    "upload-pack raw packfile response contains a control packet".into(),
5001                ));
5002            }
5003            Err(_) if input[offset..].starts_with(b"PACK") => break,
5004            Err(err) => return Err(err),
5005        }
5006    }
5007    response.packfile = input[offset..].to_vec();
5008    if response.packfile.is_empty() {
5009        return Err(GitError::InvalidFormat(
5010            "upload-pack raw packfile response missing packfile".into(),
5011        ));
5012    }
5013    if !response.packfile.starts_with(b"PACK") {
5014        return Err(GitError::InvalidFormat(
5015            "upload-pack raw packfile response packfile must start with PACK".into(),
5016        ));
5017    }
5018    Ok(response)
5019}
5020
5021pub fn encode_upload_pack_raw_packfile_response(
5022    response: &UploadPackRawPackfileResponse,
5023) -> Result<Vec<u8>> {
5024    if response.packfile.is_empty() {
5025        return Err(GitError::InvalidFormat(
5026            "upload-pack raw packfile response missing packfile".into(),
5027        ));
5028    }
5029    if !response.packfile.starts_with(b"PACK") {
5030        return Err(GitError::InvalidFormat(
5031            "upload-pack raw packfile response packfile must start with PACK".into(),
5032        ));
5033    }
5034    let mut out = Vec::new();
5035    for acknowledgment in &response.acknowledgments {
5036        write_pkt_line_payload(
5037            &mut out,
5038            &encode_upload_pack_acknowledgment(acknowledgment)?,
5039        )?;
5040    }
5041    out.extend_from_slice(&response.packfile);
5042    Ok(out)
5043}
5044
5045pub fn read_upload_pack_raw_packfile_response(
5046    format: ObjectFormat,
5047    reader: &mut impl Read,
5048) -> Result<UploadPackRawPackfileResponse> {
5049    let mut input = Vec::new();
5050    reader.read_to_end(&mut input)?;
5051    parse_upload_pack_raw_packfile_response(format, &input)
5052}
5053
5054pub fn write_upload_pack_raw_packfile_response(
5055    writer: &mut impl Write,
5056    response: &UploadPackRawPackfileResponse,
5057) -> Result<()> {
5058    if response.packfile.is_empty() {
5059        return Err(GitError::InvalidFormat(
5060            "upload-pack raw packfile response missing packfile".into(),
5061        ));
5062    }
5063    if !response.packfile.starts_with(b"PACK") {
5064        return Err(GitError::InvalidFormat(
5065            "upload-pack raw packfile response packfile must start with PACK".into(),
5066        ));
5067    }
5068    for acknowledgment in &response.acknowledgments {
5069        write_upload_pack_acknowledgment(writer, acknowledgment)?;
5070    }
5071    writer.write_all(&response.packfile)?;
5072    Ok(())
5073}
5074
5075/// Parse the smart-HTTP/SSH v0 *shallow-info* section that precedes the packfile
5076/// when the upload-pack request carried `shallow`/`deepen`/`deepen-since`/
5077/// `deepen-not` arguments.
5078///
5079/// The section is zero or more `shallow <oid>` / `unshallow <oid>` pkt-lines
5080/// terminated by a flush-pkt; git always emits it (even empty, as a bare flush)
5081/// when the request was a deepen request, and never emits it otherwise. Returns
5082/// the parsed entries and the number of bytes consumed (through the flush) so the
5083/// caller can continue parsing the trailing acknowledgments + packfile from
5084/// `&input[consumed..]` (see [`parse_upload_pack_shallow_info_and_raw_packfile_response`]).
5085pub fn parse_upload_pack_shallow_info_section(
5086    format: ObjectFormat,
5087    input: &[u8],
5088) -> Result<(Vec<ProtocolV2FetchShallowInfo>, usize)> {
5089    let mut entries = Vec::new();
5090    let mut offset = 0usize;
5091    loop {
5092        let (frame, consumed) = PktLineFrame::parse(&input[offset..])?;
5093        offset += consumed;
5094        match frame {
5095            PktLineFrame::Data(payload) => {
5096                entries.push(parse_fetch_shallow_info(format, &payload)?)
5097            }
5098            PktLineFrame::Flush => return Ok((entries, offset)),
5099            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5100                return Err(GitError::InvalidFormat(
5101                    "upload-pack shallow-info section contains a non-flush control packet".into(),
5102                ));
5103            }
5104        }
5105    }
5106}
5107
5108/// Parse a raw upload-pack response that begins with a *shallow-info* section,
5109/// i.e. the response to a deepen request.
5110///
5111/// This is [`parse_upload_pack_raw_packfile_response`] preceded by the
5112/// shallow-info section ([`parse_upload_pack_shallow_info_section`]): it returns
5113/// the `shallow`/`unshallow` entries the server reported alongside the parsed
5114/// acknowledgments + raw packfile. Use it only when the request carried a
5115/// `shallow`/`deepen`/`deepen-since`/`deepen-not` argument; for a plain (non-deepen)
5116/// request the response has no leading shallow-info section and
5117/// [`parse_upload_pack_raw_packfile_response`] must be used instead.
5118pub fn parse_upload_pack_shallow_info_and_raw_packfile_response(
5119    format: ObjectFormat,
5120    input: &[u8],
5121) -> Result<(
5122    Vec<ProtocolV2FetchShallowInfo>,
5123    UploadPackRawPackfileResponse,
5124)> {
5125    let (shallow, consumed) = parse_upload_pack_shallow_info_section(format, input)?;
5126    let response = parse_upload_pack_raw_packfile_response(format, &input[consumed..])?;
5127    Ok((shallow, response))
5128}
5129
5130/// Read a raw upload-pack response that begins with a *shallow-info* section from
5131/// `reader`, returning the `shallow`/`unshallow` entries and the parsed
5132/// acknowledgments + raw packfile.
5133///
5134/// The reader counterpart of
5135/// [`parse_upload_pack_shallow_info_and_raw_packfile_response`]; see it for when
5136/// this applies.
5137pub fn read_upload_pack_shallow_info_and_raw_packfile_response(
5138    format: ObjectFormat,
5139    reader: &mut impl Read,
5140) -> Result<(
5141    Vec<ProtocolV2FetchShallowInfo>,
5142    UploadPackRawPackfileResponse,
5143)> {
5144    let mut input = Vec::new();
5145    reader.read_to_end(&mut input)?;
5146    parse_upload_pack_shallow_info_and_raw_packfile_response(format, &input)
5147}
5148
5149pub fn parse_receive_pack_request(
5150    format: ObjectFormat,
5151    frames: &[PktLineFrame],
5152) -> Result<ReceivePackRequest> {
5153    let mut request = ReceivePackRequest::default();
5154    let mut saw_command = false;
5155    let mut saw_flush = false;
5156    for (idx, frame) in frames.iter().enumerate() {
5157        match frame {
5158            PktLineFrame::Data(payload) if !saw_flush => {
5159                let payload = trim_trailing_lf(payload);
5160                if payload.is_empty() {
5161                    return Err(GitError::InvalidFormat(
5162                        "receive-pack request line is empty".into(),
5163                    ));
5164                }
5165                if let Some(shallow) = payload.strip_prefix(b"shallow ") {
5166                    if saw_command {
5167                        return Err(GitError::InvalidFormat(
5168                            "receive-pack request has shallow after commands".into(),
5169                        ));
5170                    }
5171                    let shallow = std::str::from_utf8(shallow)
5172                        .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
5173                    validate_protocol_v2_token("receive-pack shallow", shallow)?;
5174                    request.shallow.push(ObjectId::from_hex(format, shallow)?);
5175                    continue;
5176                }
5177
5178                let (command, capabilities) = match payload.iter().position(|byte| *byte == 0) {
5179                    Some(nul) if !saw_command => (
5180                        &payload[..nul],
5181                        Some(parse_capabilities(&payload[nul + 1..])?),
5182                    ),
5183                    Some(_) => {
5184                        return Err(GitError::InvalidFormat(
5185                            "receive-pack capabilities must appear on the first command".into(),
5186                        ));
5187                    }
5188                    None => (payload, None),
5189                };
5190                let command = parse_receive_pack_command(format, command)?;
5191                if let Some(capabilities) = capabilities {
5192                    request.capabilities = capabilities;
5193                }
5194                request.commands.push(command);
5195                saw_command = true;
5196            }
5197            PktLineFrame::Data(_) => {
5198                return Err(GitError::InvalidFormat(
5199                    "receive-pack request has data after flush".into(),
5200                ));
5201            }
5202            PktLineFrame::Flush => {
5203                saw_flush = true;
5204                if idx + 1 != frames.len() {
5205                    return Err(GitError::InvalidFormat(
5206                        "receive-pack request has frames after flush".into(),
5207                    ));
5208                }
5209            }
5210            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5211                return Err(GitError::InvalidFormat(
5212                    "receive-pack request contains a non-flush control packet".into(),
5213                ));
5214            }
5215        }
5216    }
5217    if !saw_flush {
5218        return Err(GitError::InvalidFormat(
5219            "receive-pack request missing flush".into(),
5220        ));
5221    }
5222    if !request.shallow.is_empty() && request.commands.is_empty() {
5223        return Err(GitError::InvalidFormat(
5224            "receive-pack request has shallow lines without commands".into(),
5225        ));
5226    }
5227    Ok(request)
5228}
5229
5230pub fn encode_receive_pack_request(request: &ReceivePackRequest) -> Result<Vec<PktLineFrame>> {
5231    if !request.shallow.is_empty() && request.commands.is_empty() {
5232        return Err(GitError::InvalidFormat(
5233            "receive-pack request has shallow lines without commands".into(),
5234        ));
5235    }
5236
5237    let mut frames = Vec::new();
5238    for oid in &request.shallow {
5239        frames.push(PktLineFrame::data(line_from_str(&format!(
5240            "shallow {oid}"
5241        )))?);
5242    }
5243    for (idx, command) in request.commands.iter().enumerate() {
5244        let mut payload = format_receive_pack_command(command)?;
5245        if idx == 0 && !request.capabilities.is_empty() {
5246            payload.push(0);
5247            payload.extend_from_slice(&encode_capabilities(&request.capabilities)?);
5248        }
5249        payload.push(b'\n');
5250        frames.push(PktLineFrame::data(payload)?);
5251    }
5252    frames.push(PktLineFrame::Flush);
5253    Ok(frames)
5254}
5255
5256pub fn read_receive_pack_request(
5257    format: ObjectFormat,
5258    reader: &mut impl Read,
5259) -> Result<ReceivePackRequest> {
5260    let frames = read_pkt_line_frames_until_flush(reader)?;
5261    parse_receive_pack_request(format, &frames)
5262}
5263
5264pub fn write_receive_pack_request(
5265    writer: &mut impl Write,
5266    request: &ReceivePackRequest,
5267) -> Result<()> {
5268    if !request.shallow.is_empty() && request.commands.is_empty() {
5269        return Err(GitError::InvalidFormat(
5270            "receive-pack request has shallow lines without commands".into(),
5271        ));
5272    }
5273
5274    for oid in &request.shallow {
5275        write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
5276    }
5277    for (idx, command) in request.commands.iter().enumerate() {
5278        let mut payload = format_receive_pack_command(command)?;
5279        if idx == 0 && !request.capabilities.is_empty() {
5280            payload.push(0);
5281            payload.extend_from_slice(&encode_capabilities(&request.capabilities)?);
5282        }
5283        payload.push(b'\n');
5284        write_pkt_line_payload(writer, &payload)?;
5285    }
5286    writer.write_all(b"0000")?;
5287    Ok(())
5288}
5289
5290pub fn parse_receive_pack_push_request(
5291    format: ObjectFormat,
5292    input: &[u8],
5293    has_push_options: bool,
5294) -> Result<ReceivePackPushRequest> {
5295    let (command_frames, consumed) = parse_pkt_line_frames_until_flush_from(input)?;
5296    let commands = parse_receive_pack_request(format, &command_frames)?;
5297    let mut offset = consumed;
5298    let push_options = if has_push_options {
5299        let (push_option_frames, consumed) =
5300            parse_pkt_line_frames_until_flush_from(&input[offset..])?;
5301        offset += consumed;
5302        Some(parse_receive_pack_push_options(&push_option_frames)?)
5303    } else {
5304        None
5305    };
5306    Ok(ReceivePackPushRequest {
5307        commands,
5308        push_options,
5309        packfile: input[offset..].to_vec(),
5310    })
5311}
5312
5313pub fn encode_receive_pack_push_request(request: &ReceivePackPushRequest) -> Result<Vec<u8>> {
5314    let mut out = Vec::new();
5315    write_receive_pack_request(&mut out, &request.commands)?;
5316    if let Some(push_options) = &request.push_options {
5317        write_receive_pack_push_options(&mut out, push_options)?;
5318    }
5319    out.extend_from_slice(&request.packfile);
5320    Ok(out)
5321}
5322
5323pub fn read_receive_pack_push_request(
5324    format: ObjectFormat,
5325    reader: &mut impl Read,
5326    has_push_options: bool,
5327) -> Result<ReceivePackPushRequest> {
5328    let commands = read_receive_pack_request(format, reader)?;
5329    let push_options = if has_push_options {
5330        Some(read_receive_pack_push_options(reader)?)
5331    } else {
5332        None
5333    };
5334    let mut packfile = Vec::new();
5335    reader.read_to_end(&mut packfile)?;
5336    Ok(ReceivePackPushRequest {
5337        commands,
5338        push_options,
5339        packfile,
5340    })
5341}
5342
5343pub fn write_receive_pack_push_request(
5344    writer: &mut impl Write,
5345    request: &ReceivePackPushRequest,
5346) -> Result<()> {
5347    write_receive_pack_request(writer, &request.commands)?;
5348    if let Some(push_options) = &request.push_options {
5349        write_receive_pack_push_options(writer, push_options)?;
5350    }
5351    writer.write_all(&request.packfile)?;
5352    Ok(())
5353}
5354
5355pub fn parse_receive_pack_features(capabilities: &[Capability]) -> Result<ReceivePackFeatures> {
5356    let mut features = ReceivePackFeatures::default();
5357    for capability in capabilities {
5358        match capability.name.as_str() {
5359            "report-status" => {
5360                reject_capability_value("receive-pack report-status", capability)?;
5361                if features.report_status {
5362                    return Err(GitError::InvalidFormat(
5363                        "receive-pack has duplicate report-status capability".into(),
5364                    ));
5365                }
5366                features.report_status = true;
5367            }
5368            "report-status-v2" => {
5369                reject_capability_value("receive-pack report-status-v2", capability)?;
5370                if features.report_status_v2 {
5371                    return Err(GitError::InvalidFormat(
5372                        "receive-pack has duplicate report-status-v2 capability".into(),
5373                    ));
5374                }
5375                features.report_status_v2 = true;
5376            }
5377            "delete-refs" => {
5378                reject_capability_value("receive-pack delete-refs", capability)?;
5379                if features.delete_refs {
5380                    return Err(GitError::InvalidFormat(
5381                        "receive-pack has duplicate delete-refs capability".into(),
5382                    ));
5383                }
5384                features.delete_refs = true;
5385            }
5386            "ofs-delta" => {
5387                reject_capability_value("receive-pack ofs-delta", capability)?;
5388                if features.ofs_delta {
5389                    return Err(GitError::InvalidFormat(
5390                        "receive-pack has duplicate ofs-delta capability".into(),
5391                    ));
5392                }
5393                features.ofs_delta = true;
5394            }
5395            "atomic" => {
5396                reject_capability_value("receive-pack atomic", capability)?;
5397                if features.atomic {
5398                    return Err(GitError::InvalidFormat(
5399                        "receive-pack has duplicate atomic capability".into(),
5400                    ));
5401                }
5402                features.atomic = true;
5403            }
5404            "push-options" => {
5405                reject_capability_value("receive-pack push-options", capability)?;
5406                if features.push_options {
5407                    return Err(GitError::InvalidFormat(
5408                        "receive-pack has duplicate push-options capability".into(),
5409                    ));
5410                }
5411                features.push_options = true;
5412            }
5413            "side-band-64k" => {
5414                reject_capability_value("receive-pack side-band-64k", capability)?;
5415                if features.side_band_64k {
5416                    return Err(GitError::InvalidFormat(
5417                        "receive-pack has duplicate side-band-64k capability".into(),
5418                    ));
5419                }
5420                features.side_band_64k = true;
5421            }
5422            "quiet" => {
5423                reject_capability_value("receive-pack quiet", capability)?;
5424                if features.quiet {
5425                    return Err(GitError::InvalidFormat(
5426                        "receive-pack has duplicate quiet capability".into(),
5427                    ));
5428                }
5429                features.quiet = true;
5430            }
5431            "no-thin" => {
5432                reject_capability_value("receive-pack no-thin", capability)?;
5433                if features.no_thin {
5434                    return Err(GitError::InvalidFormat(
5435                        "receive-pack has duplicate no-thin capability".into(),
5436                    ));
5437                }
5438                features.no_thin = true;
5439            }
5440            "agent" => {
5441                let Some(agent) = &capability.value else {
5442                    return Err(GitError::InvalidFormat(
5443                        "receive-pack agent capability is missing value".into(),
5444                    ));
5445                };
5446                if features.agent.is_some() {
5447                    return Err(GitError::InvalidFormat(
5448                        "receive-pack has duplicate agent capability".into(),
5449                    ));
5450                }
5451                validate_capability_field("receive-pack agent", agent)?;
5452                features.agent = Some(agent.clone());
5453            }
5454            "object-format" => {
5455                let Some(format) = &capability.value else {
5456                    return Err(GitError::InvalidFormat(
5457                        "receive-pack object-format capability is missing value".into(),
5458                    ));
5459                };
5460                if features.object_format.is_some() {
5461                    return Err(GitError::InvalidFormat(
5462                        "receive-pack has duplicate object-format capability".into(),
5463                    ));
5464                }
5465                validate_capability_field("receive-pack object-format", format)?;
5466                features.object_format = Some(format.parse()?);
5467            }
5468            _ => {
5469                encode_capabilities(std::slice::from_ref(capability))?;
5470                if features
5471                    .unknown
5472                    .iter()
5473                    .any(|known| known.name == capability.name)
5474                {
5475                    return Err(GitError::InvalidFormat(format!(
5476                        "receive-pack has duplicate {} capability",
5477                        capability.name
5478                    )));
5479                }
5480                features.unknown.push(capability.clone());
5481            }
5482        }
5483    }
5484    Ok(features)
5485}
5486
5487pub fn encode_receive_pack_features(features: &ReceivePackFeatures) -> Result<Vec<Capability>> {
5488    let mut capabilities = Vec::new();
5489    if features.report_status {
5490        capabilities.push(Capability {
5491            name: "report-status".into(),
5492            value: None,
5493        });
5494    }
5495    if features.report_status_v2 {
5496        capabilities.push(Capability {
5497            name: "report-status-v2".into(),
5498            value: None,
5499        });
5500    }
5501    if features.delete_refs {
5502        capabilities.push(Capability {
5503            name: "delete-refs".into(),
5504            value: None,
5505        });
5506    }
5507    if features.ofs_delta {
5508        capabilities.push(Capability {
5509            name: "ofs-delta".into(),
5510            value: None,
5511        });
5512    }
5513    if features.atomic {
5514        capabilities.push(Capability {
5515            name: "atomic".into(),
5516            value: None,
5517        });
5518    }
5519    if features.push_options {
5520        capabilities.push(Capability {
5521            name: "push-options".into(),
5522            value: None,
5523        });
5524    }
5525    if features.side_band_64k {
5526        capabilities.push(Capability {
5527            name: "side-band-64k".into(),
5528            value: None,
5529        });
5530    }
5531    if features.quiet {
5532        capabilities.push(Capability {
5533            name: "quiet".into(),
5534            value: None,
5535        });
5536    }
5537    if features.no_thin {
5538        capabilities.push(Capability {
5539            name: "no-thin".into(),
5540            value: None,
5541        });
5542    }
5543    if let Some(agent) = &features.agent {
5544        validate_capability_field("receive-pack agent", agent)?;
5545        capabilities.push(Capability {
5546            name: "agent".into(),
5547            value: Some(agent.clone()),
5548        });
5549    }
5550    if let Some(format) = features.object_format {
5551        capabilities.push(Capability {
5552            name: "object-format".into(),
5553            value: Some(format.name().into()),
5554        });
5555    }
5556    for capability in &features.unknown {
5557        if is_known_receive_pack_capability(&capability.name) {
5558            return Err(GitError::InvalidFormat(format!(
5559                "receive-pack unknown capability duplicates known capability {}",
5560                capability.name
5561            )));
5562        }
5563        encode_capabilities(std::slice::from_ref(capability))?;
5564        capabilities.push(capability.clone());
5565    }
5566    Ok(capabilities)
5567}
5568
5569pub fn validate_receive_pack_push_request_features(
5570    features: &ReceivePackFeatures,
5571    request: &ReceivePackPushRequest,
5572) -> Result<()> {
5573    for capability in &request.commands.capabilities {
5574        if matches!(
5575            capability.name.as_str(),
5576            "report-status"
5577                | "report-status-v2"
5578                | "delete-refs"
5579                | "ofs-delta"
5580                | "atomic"
5581                | "push-options"
5582                | "side-band-64k"
5583                | "quiet"
5584                | "no-thin"
5585        ) {
5586            reject_capability_value("receive-pack request capability", capability)?;
5587        }
5588        match capability.name.as_str() {
5589            "report-status" if !features.report_status => {
5590                return Err(GitError::InvalidFormat(
5591                    "receive-pack request uses report-status without advertised capability".into(),
5592                ));
5593            }
5594            "report-status-v2" if !features.report_status_v2 => {
5595                return Err(GitError::InvalidFormat(
5596                    "receive-pack request uses report-status-v2 without advertised capability"
5597                        .into(),
5598                ));
5599            }
5600            "delete-refs" if !features.delete_refs => {
5601                return Err(GitError::InvalidFormat(
5602                    "receive-pack request uses delete-refs without advertised capability".into(),
5603                ));
5604            }
5605            "ofs-delta" if !features.ofs_delta => {
5606                return Err(GitError::InvalidFormat(
5607                    "receive-pack request uses ofs-delta without advertised capability".into(),
5608                ));
5609            }
5610            "atomic" if !features.atomic => {
5611                return Err(GitError::InvalidFormat(
5612                    "receive-pack request uses atomic without advertised capability".into(),
5613                ));
5614            }
5615            "push-options" if !features.push_options => {
5616                return Err(GitError::InvalidFormat(
5617                    "receive-pack request uses push-options without advertised capability".into(),
5618                ));
5619            }
5620            "side-band-64k" if !features.side_band_64k => {
5621                return Err(GitError::InvalidFormat(
5622                    "receive-pack request uses side-band-64k without advertised capability".into(),
5623                ));
5624            }
5625            "quiet" if !features.quiet => {
5626                return Err(GitError::InvalidFormat(
5627                    "receive-pack request uses quiet without advertised capability".into(),
5628                ));
5629            }
5630            "no-thin" => {
5631                return Err(GitError::InvalidFormat(
5632                    "receive-pack request must not request no-thin".into(),
5633                ));
5634            }
5635            "agent" => {
5636                validate_capability_field(
5637                    "receive-pack request agent",
5638                    capability.value.as_deref().unwrap_or_default(),
5639                )?;
5640            }
5641            "object-format" => {
5642                let Some(value) = &capability.value else {
5643                    return Err(GitError::InvalidFormat(
5644                        "receive-pack request object-format capability is missing value".into(),
5645                    ));
5646                };
5647                let requested_format: ObjectFormat = value.parse()?;
5648                if features.object_format != Some(requested_format) {
5649                    return Err(GitError::InvalidFormat(
5650                        "receive-pack request object-format was not advertised".into(),
5651                    ));
5652                }
5653            }
5654            name if is_known_receive_pack_capability(name) => {}
5655            _ => {
5656                if !features
5657                    .unknown
5658                    .iter()
5659                    .any(|advertised| advertised.name == capability.name)
5660                {
5661                    return Err(GitError::InvalidFormat(format!(
5662                        "receive-pack request uses unadvertised capability {}",
5663                        capability.name
5664                    )));
5665                }
5666            }
5667        }
5668    }
5669
5670    let requested_push_options = request
5671        .commands
5672        .capabilities
5673        .iter()
5674        .any(|capability| capability.name == "push-options");
5675    match (requested_push_options, &request.push_options) {
5676        (true, Some(_)) => {}
5677        (true, None) => {
5678            return Err(GitError::InvalidFormat(
5679                "receive-pack request uses push-options without push-options section".into(),
5680            ));
5681        }
5682        (false, Some(_)) => {
5683            return Err(GitError::InvalidFormat(
5684                "receive-pack request has push-options section without requested capability".into(),
5685            ));
5686        }
5687        (false, None) => {}
5688    }
5689
5690    let has_delete = request
5691        .commands
5692        .commands
5693        .iter()
5694        .any(is_receive_pack_delete_command);
5695    if has_delete && !features.delete_refs {
5696        return Err(GitError::InvalidFormat(
5697            "receive-pack request deletes refs without advertised delete-refs capability".into(),
5698        ));
5699    }
5700
5701    let has_update_or_create = request
5702        .commands
5703        .commands
5704        .iter()
5705        .any(|command| !is_receive_pack_delete_command(command));
5706    if !has_update_or_create && !request.packfile.is_empty() {
5707        return Err(GitError::InvalidFormat(
5708            "receive-pack delete-only request must not include packfile".into(),
5709        ));
5710    }
5711    Ok(())
5712}
5713
5714pub fn apply_receive_pack_push_request<R, I, C, U, D>(
5715    features: &ReceivePackFeatures,
5716    request: &ReceivePackPushRequest,
5717    mut read_ref: R,
5718    mut install_pack: I,
5719    mut contains_object: C,
5720    mut apply_updates: U,
5721    mut delete_ref: D,
5722) -> Result<ReceivePackReportStatus>
5723where
5724    R: FnMut(&str) -> Result<Option<ObjectId>>,
5725    I: FnMut(&[u8]) -> Result<()>,
5726    C: FnMut(&ObjectId) -> Result<bool>,
5727    U: FnMut(&[ReceivePackCommand]) -> Result<()>,
5728    D: FnMut(&ReceivePackCommand) -> Result<()>,
5729{
5730    validate_receive_pack_push_request_features(features, request)?;
5731
5732    for command in request
5733        .commands
5734        .commands
5735        .iter()
5736        .filter(|command| is_receive_pack_delete_command(command))
5737    {
5738        if !command.old_id.is_null() && read_ref(&command.name)? != Some(command.old_id.clone()) {
5739            return Err(GitError::Transaction(format!(
5740                "expected ref {} to match",
5741                command.name
5742            )));
5743        }
5744    }
5745
5746    let updates = request
5747        .commands
5748        .commands
5749        .iter()
5750        .filter(|command| !is_receive_pack_delete_command(command))
5751        .cloned()
5752        .collect::<Vec<_>>();
5753    if !updates.is_empty() {
5754        if !request.packfile.is_empty() {
5755            install_pack(&request.packfile)?;
5756        }
5757        for command in &updates {
5758            if !contains_object(&command.new_id)? {
5759                return Err(GitError::InvalidObject(format!(
5760                    "receive-pack packfile did not provide {}",
5761                    command.new_id
5762                )));
5763            }
5764        }
5765        apply_updates(&updates)?;
5766    }
5767
5768    for command in request
5769        .commands
5770        .commands
5771        .iter()
5772        .filter(|command| is_receive_pack_delete_command(command))
5773    {
5774        delete_ref(command)?;
5775    }
5776
5777    Ok(ReceivePackReportStatus {
5778        unpack: ReceivePackUnpackStatus::Ok,
5779        commands: request
5780            .commands
5781            .commands
5782            .iter()
5783            .map(|command| ReceivePackCommandStatus::Ok {
5784                name: command.name.clone(),
5785            })
5786            .collect(),
5787    })
5788}
5789
5790pub fn parse_receive_pack_report_status(
5791    frames: &[PktLineFrame],
5792) -> Result<ReceivePackReportStatus> {
5793    let Some((first, rest)) = frames.split_first() else {
5794        return Err(GitError::InvalidFormat(
5795            "receive-pack report-status is empty".into(),
5796        ));
5797    };
5798    let PktLineFrame::Data(payload) = first else {
5799        return Err(GitError::InvalidFormat(
5800            "receive-pack report-status must start with unpack status".into(),
5801        ));
5802    };
5803    let unpack = parse_receive_pack_unpack_status(payload)?;
5804
5805    let mut commands = Vec::new();
5806    let mut saw_flush = false;
5807    for (idx, frame) in rest.iter().enumerate() {
5808        match frame {
5809            PktLineFrame::Data(payload) if !saw_flush => {
5810                commands.push(parse_receive_pack_command_status(payload)?);
5811            }
5812            PktLineFrame::Data(_) => {
5813                return Err(GitError::InvalidFormat(
5814                    "receive-pack report-status has data after flush".into(),
5815                ));
5816            }
5817            PktLineFrame::Flush => {
5818                saw_flush = true;
5819                if idx + 1 != rest.len() {
5820                    return Err(GitError::InvalidFormat(
5821                        "receive-pack report-status has frames after flush".into(),
5822                    ));
5823                }
5824            }
5825            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5826                return Err(GitError::InvalidFormat(
5827                    "receive-pack report-status contains a non-flush control packet".into(),
5828                ));
5829            }
5830        }
5831    }
5832    if !saw_flush {
5833        return Err(GitError::InvalidFormat(
5834            "receive-pack report-status missing flush".into(),
5835        ));
5836    }
5837    Ok(ReceivePackReportStatus { unpack, commands })
5838}
5839
5840pub fn encode_receive_pack_report_status(
5841    report: &ReceivePackReportStatus,
5842) -> Result<Vec<PktLineFrame>> {
5843    let mut frames = Vec::new();
5844    frames.push(PktLineFrame::data(line_from_str(
5845        &format_receive_pack_unpack_status(&report.unpack)?,
5846    ))?);
5847    for command in &report.commands {
5848        frames.push(PktLineFrame::data(line_from_str(
5849            &format_receive_pack_command_status(command)?,
5850        ))?);
5851    }
5852    frames.push(PktLineFrame::Flush);
5853    Ok(frames)
5854}
5855
5856pub fn read_receive_pack_report_status(reader: &mut impl Read) -> Result<ReceivePackReportStatus> {
5857    let frames = read_pkt_line_frames_until_flush(reader)?;
5858    parse_receive_pack_report_status(&frames)
5859}
5860
5861pub fn write_receive_pack_report_status(
5862    writer: &mut impl Write,
5863    report: &ReceivePackReportStatus,
5864) -> Result<()> {
5865    write_pkt_line_payload(
5866        writer,
5867        &line_from_str(&format_receive_pack_unpack_status(&report.unpack)?),
5868    )?;
5869    for command in &report.commands {
5870        write_pkt_line_payload(
5871            writer,
5872            &line_from_str(&format_receive_pack_command_status(command)?),
5873        )?;
5874    }
5875    writer.write_all(b"0000")?;
5876    Ok(())
5877}
5878
5879pub fn parse_receive_pack_report_status_v2(
5880    format: ObjectFormat,
5881    frames: &[PktLineFrame],
5882) -> Result<ReceivePackReportStatusV2> {
5883    let Some((first, rest)) = frames.split_first() else {
5884        return Err(GitError::InvalidFormat(
5885            "receive-pack report-status-v2 is empty".into(),
5886        ));
5887    };
5888    let PktLineFrame::Data(payload) = first else {
5889        return Err(GitError::InvalidFormat(
5890            "receive-pack report-status-v2 must start with unpack status".into(),
5891        ));
5892    };
5893    let unpack = parse_receive_pack_unpack_status(payload)?;
5894
5895    let mut commands = Vec::new();
5896    let mut saw_flush = false;
5897    for (idx, frame) in rest.iter().enumerate() {
5898        match frame {
5899            PktLineFrame::Data(payload) if !saw_flush => {
5900                let text =
5901                    parse_protocol_v2_line_text("receive-pack report-status-v2 line", payload)?;
5902                if text.starts_with("option ") {
5903                    let Some(ReceivePackCommandStatusV2::Ok { options, .. }) = commands.last_mut()
5904                    else {
5905                        return Err(GitError::InvalidFormat(
5906                            "receive-pack report-status-v2 option without ok status".into(),
5907                        ));
5908                    };
5909                    parse_receive_pack_report_status_v2_option(format, text, options)?;
5910                } else {
5911                    commands.push(parse_receive_pack_command_status_v2(text)?);
5912                }
5913            }
5914            PktLineFrame::Data(_) => {
5915                return Err(GitError::InvalidFormat(
5916                    "receive-pack report-status-v2 has data after flush".into(),
5917                ));
5918            }
5919            PktLineFrame::Flush => {
5920                saw_flush = true;
5921                if idx + 1 != rest.len() {
5922                    return Err(GitError::InvalidFormat(
5923                        "receive-pack report-status-v2 has frames after flush".into(),
5924                    ));
5925                }
5926            }
5927            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
5928                return Err(GitError::InvalidFormat(
5929                    "receive-pack report-status-v2 contains a non-flush control packet".into(),
5930                ));
5931            }
5932        }
5933    }
5934    if !saw_flush {
5935        return Err(GitError::InvalidFormat(
5936            "receive-pack report-status-v2 missing flush".into(),
5937        ));
5938    }
5939    Ok(ReceivePackReportStatusV2 { unpack, commands })
5940}
5941
5942pub fn encode_receive_pack_report_status_v2(
5943    report: &ReceivePackReportStatusV2,
5944) -> Result<Vec<PktLineFrame>> {
5945    let mut frames = Vec::new();
5946    frames.push(PktLineFrame::data(line_from_str(
5947        &format_receive_pack_unpack_status(&report.unpack)?,
5948    ))?);
5949    for command in &report.commands {
5950        frames.push(PktLineFrame::data(line_from_str(
5951            &format_receive_pack_command_status_v2(command)?,
5952        ))?);
5953        if let ReceivePackCommandStatusV2::Ok { options, .. } = command {
5954            for option in format_receive_pack_report_status_v2_options(options)? {
5955                frames.push(PktLineFrame::data(line_from_str(&option))?);
5956            }
5957        }
5958    }
5959    frames.push(PktLineFrame::Flush);
5960    Ok(frames)
5961}
5962
5963pub fn read_receive_pack_report_status_v2(
5964    format: ObjectFormat,
5965    reader: &mut impl Read,
5966) -> Result<ReceivePackReportStatusV2> {
5967    let frames = read_pkt_line_frames_until_flush(reader)?;
5968    parse_receive_pack_report_status_v2(format, &frames)
5969}
5970
5971pub fn write_receive_pack_report_status_v2(
5972    writer: &mut impl Write,
5973    report: &ReceivePackReportStatusV2,
5974) -> Result<()> {
5975    write_pkt_line_payload(
5976        writer,
5977        &line_from_str(&format_receive_pack_unpack_status(&report.unpack)?),
5978    )?;
5979    for command in &report.commands {
5980        write_pkt_line_payload(
5981            writer,
5982            &line_from_str(&format_receive_pack_command_status_v2(command)?),
5983        )?;
5984        if let ReceivePackCommandStatusV2::Ok { options, .. } = command {
5985            for option in format_receive_pack_report_status_v2_options(options)? {
5986                write_pkt_line_payload(writer, &line_from_str(&option))?;
5987            }
5988        }
5989    }
5990    writer.write_all(b"0000")?;
5991    Ok(())
5992}
5993
5994pub fn parse_receive_pack_push_options(frames: &[PktLineFrame]) -> Result<Vec<String>> {
5995    let mut options = Vec::new();
5996    let mut saw_flush = false;
5997    for (idx, frame) in frames.iter().enumerate() {
5998        match frame {
5999            PktLineFrame::Data(payload) if !saw_flush => {
6000                let option = trim_trailing_lf(payload);
6001                validate_receive_pack_push_option(option)?;
6002                options.push(
6003                    std::str::from_utf8(option)
6004                        .map_err(|err| GitError::InvalidFormat(err.to_string()))?
6005                        .to_string(),
6006                );
6007            }
6008            PktLineFrame::Data(_) => {
6009                return Err(GitError::InvalidFormat(
6010                    "receive-pack push-options has data after flush".into(),
6011                ));
6012            }
6013            PktLineFrame::Flush => {
6014                saw_flush = true;
6015                if idx + 1 != frames.len() {
6016                    return Err(GitError::InvalidFormat(
6017                        "receive-pack push-options has frames after flush".into(),
6018                    ));
6019                }
6020            }
6021            PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
6022                return Err(GitError::InvalidFormat(
6023                    "receive-pack push-options contains a non-flush control packet".into(),
6024                ));
6025            }
6026        }
6027    }
6028    if !saw_flush {
6029        return Err(GitError::InvalidFormat(
6030            "receive-pack push-options missing flush".into(),
6031        ));
6032    }
6033    Ok(options)
6034}
6035
6036pub fn encode_receive_pack_push_options(options: &[String]) -> Result<Vec<PktLineFrame>> {
6037    let mut frames = Vec::new();
6038    for option in options {
6039        validate_receive_pack_push_option(option.as_bytes())?;
6040        let mut payload = option.as_bytes().to_vec();
6041        payload.push(b'\n');
6042        frames.push(PktLineFrame::data(payload)?);
6043    }
6044    frames.push(PktLineFrame::Flush);
6045    Ok(frames)
6046}
6047
6048pub fn read_receive_pack_push_options(reader: &mut impl Read) -> Result<Vec<String>> {
6049    let frames = read_pkt_line_frames_until_flush(reader)?;
6050    parse_receive_pack_push_options(&frames)
6051}
6052
6053pub fn write_receive_pack_push_options(writer: &mut impl Write, options: &[String]) -> Result<()> {
6054    for option in options {
6055        validate_receive_pack_push_option(option.as_bytes())?;
6056        let mut payload = option.as_bytes().to_vec();
6057        payload.push(b'\n');
6058        write_pkt_line_payload(writer, &payload)?;
6059    }
6060    writer.write_all(b"0000")?;
6061    Ok(())
6062}
6063
6064fn parse_receive_pack_command(format: ObjectFormat, payload: &[u8]) -> Result<ReceivePackCommand> {
6065    let text =
6066        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6067    let mut fields = text.split(' ');
6068    let old_id = fields
6069        .next()
6070        .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing old id".into()))?;
6071    let new_id = fields
6072        .next()
6073        .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing new id".into()))?;
6074    let name = fields
6075        .next()
6076        .ok_or_else(|| GitError::InvalidFormat("receive-pack command missing ref name".into()))?;
6077    if fields.next().is_some() {
6078        return Err(GitError::InvalidFormat(
6079            "receive-pack command has too many fields".into(),
6080        ));
6081    }
6082    validate_protocol_v2_token("receive-pack old id", old_id)?;
6083    validate_protocol_v2_token("receive-pack new id", new_id)?;
6084    validate_protocol_v2_token("receive-pack ref name", name)?;
6085    Ok(ReceivePackCommand {
6086        old_id: ObjectId::from_hex(format, old_id)?,
6087        new_id: ObjectId::from_hex(format, new_id)?,
6088        name: name.to_string(),
6089    })
6090}
6091
6092fn format_receive_pack_command(command: &ReceivePackCommand) -> Result<Vec<u8>> {
6093    if command.old_id.format() != command.new_id.format() {
6094        return Err(GitError::InvalidObjectId(
6095            "receive-pack command object formats do not match".into(),
6096        ));
6097    }
6098    validate_protocol_v2_token("receive-pack ref name", &command.name)?;
6099    Ok(format!("{} {} {}", command.old_id, command.new_id, command.name).into_bytes())
6100}
6101
6102fn set_upload_pack_flag(value: &mut bool, capability: &Capability) -> Result<()> {
6103    reject_capability_value("upload-pack capability", capability)?;
6104    if *value {
6105        return Err(GitError::InvalidFormat(format!(
6106            "upload-pack has duplicate {} capability",
6107            capability.name
6108        )));
6109    }
6110    *value = true;
6111    Ok(())
6112}
6113
6114fn push_upload_pack_flag(capabilities: &mut Vec<Capability>, name: &str, enabled: bool) {
6115    if enabled {
6116        capabilities.push(Capability {
6117            name: name.into(),
6118            value: None,
6119        });
6120    }
6121}
6122
6123fn is_known_upload_pack_capability(name: &str) -> bool {
6124    matches!(
6125        name,
6126        "multi_ack"
6127            | "multi_ack_detailed"
6128            | "no-done"
6129            | "thin-pack"
6130            | "side-band"
6131            | "side-band-64k"
6132            | "ofs-delta"
6133            | "shallow"
6134            | "deepen-since"
6135            | "deepen-not"
6136            | "include-tag"
6137            | "no-progress"
6138            | "allow-tip-sha1-in-want"
6139            | "allow-reachable-sha1-in-want"
6140            | "filter"
6141            | "agent"
6142            | "object-format"
6143            | "symref"
6144    )
6145}
6146
6147fn is_upload_pack_flag_capability(name: &str) -> bool {
6148    matches!(
6149        name,
6150        "multi_ack"
6151            | "multi_ack_detailed"
6152            | "no-done"
6153            | "thin-pack"
6154            | "side-band"
6155            | "side-band-64k"
6156            | "ofs-delta"
6157            | "shallow"
6158            | "deepen-since"
6159            | "deepen-not"
6160            | "include-tag"
6161            | "no-progress"
6162            | "allow-tip-sha1-in-want"
6163            | "allow-reachable-sha1-in-want"
6164            | "filter"
6165    )
6166}
6167
6168fn reject_capability_value(label: &str, capability: &Capability) -> Result<()> {
6169    if capability.value.is_some() {
6170        return Err(GitError::InvalidFormat(format!(
6171            "{label} must not have value"
6172        )));
6173    }
6174    Ok(())
6175}
6176
6177fn is_known_receive_pack_capability(name: &str) -> bool {
6178    matches!(
6179        name,
6180        "report-status"
6181            | "report-status-v2"
6182            | "delete-refs"
6183            | "ofs-delta"
6184            | "atomic"
6185            | "push-options"
6186            | "side-band-64k"
6187            | "quiet"
6188            | "no-thin"
6189            | "agent"
6190            | "object-format"
6191    )
6192}
6193
6194fn is_receive_pack_delete_command(command: &ReceivePackCommand) -> bool {
6195    command.new_id.is_null()
6196}
6197
6198fn parse_receive_pack_unpack_status(payload: &[u8]) -> Result<ReceivePackUnpackStatus> {
6199    let text = parse_protocol_v2_line_text("receive-pack unpack status", payload)?;
6200    if text == "unpack ok" {
6201        return Ok(ReceivePackUnpackStatus::Ok);
6202    }
6203    let Some(message) = text.strip_prefix("unpack ") else {
6204        return Err(GitError::InvalidFormat(format!(
6205            "unsupported receive-pack unpack status {text}"
6206        )));
6207    };
6208    validate_receive_pack_status_message("receive-pack unpack error", message)?;
6209    Ok(ReceivePackUnpackStatus::Error(message.to_string()))
6210}
6211
6212fn format_receive_pack_unpack_status(status: &ReceivePackUnpackStatus) -> Result<String> {
6213    match status {
6214        ReceivePackUnpackStatus::Ok => Ok("unpack ok".into()),
6215        ReceivePackUnpackStatus::Error(message) => {
6216            validate_receive_pack_status_message("receive-pack unpack error", message)?;
6217            Ok(format!("unpack {message}"))
6218        }
6219    }
6220}
6221
6222fn parse_receive_pack_command_status(payload: &[u8]) -> Result<ReceivePackCommandStatus> {
6223    let text = parse_protocol_v2_line_text("receive-pack command status", payload)?;
6224    if let Some(name) = text.strip_prefix("ok ") {
6225        validate_protocol_v2_token("receive-pack status ref name", name)?;
6226        return Ok(ReceivePackCommandStatus::Ok {
6227            name: name.to_string(),
6228        });
6229    }
6230    let Some(rest) = text.strip_prefix("ng ") else {
6231        return Err(GitError::InvalidFormat(format!(
6232            "unsupported receive-pack command status {text}"
6233        )));
6234    };
6235    let (name, message) = rest
6236        .split_once(' ')
6237        .ok_or_else(|| GitError::InvalidFormat("receive-pack ng status missing message".into()))?;
6238    validate_protocol_v2_token("receive-pack status ref name", name)?;
6239    validate_receive_pack_status_message("receive-pack ng status message", message)?;
6240    Ok(ReceivePackCommandStatus::Ng {
6241        name: name.to_string(),
6242        message: message.to_string(),
6243    })
6244}
6245
6246fn format_receive_pack_command_status(status: &ReceivePackCommandStatus) -> Result<String> {
6247    match status {
6248        ReceivePackCommandStatus::Ok { name } => {
6249            validate_protocol_v2_token("receive-pack status ref name", name)?;
6250            Ok(format!("ok {name}"))
6251        }
6252        ReceivePackCommandStatus::Ng { name, message } => {
6253            validate_protocol_v2_token("receive-pack status ref name", name)?;
6254            validate_receive_pack_status_message("receive-pack ng status message", message)?;
6255            Ok(format!("ng {name} {message}"))
6256        }
6257    }
6258}
6259
6260fn parse_receive_pack_command_status_v2(text: &str) -> Result<ReceivePackCommandStatusV2> {
6261    if let Some(name) = text.strip_prefix("ok ") {
6262        validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6263        return Ok(ReceivePackCommandStatusV2::Ok {
6264            name: name.to_string(),
6265            options: ReceivePackCommandStatusV2Options::default(),
6266        });
6267    }
6268    let Some(rest) = text.strip_prefix("ng ") else {
6269        return Err(GitError::InvalidFormat(format!(
6270            "unsupported receive-pack report-status-v2 line {text}"
6271        )));
6272    };
6273    let (name, message) = rest.split_once(' ').ok_or_else(|| {
6274        GitError::InvalidFormat("receive-pack status-v2 ng status missing message".into())
6275    })?;
6276    validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6277    validate_receive_pack_status_message("receive-pack status-v2 ng message", message)?;
6278    Ok(ReceivePackCommandStatusV2::Ng {
6279        name: name.to_string(),
6280        message: message.to_string(),
6281    })
6282}
6283
6284fn format_receive_pack_command_status_v2(status: &ReceivePackCommandStatusV2) -> Result<String> {
6285    match status {
6286        ReceivePackCommandStatusV2::Ok { name, .. } => {
6287            validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6288            Ok(format!("ok {name}"))
6289        }
6290        ReceivePackCommandStatusV2::Ng { name, message } => {
6291            validate_protocol_v2_token("receive-pack status-v2 ref name", name)?;
6292            validate_receive_pack_status_message("receive-pack status-v2 ng message", message)?;
6293            Ok(format!("ng {name} {message}"))
6294        }
6295    }
6296}
6297
6298fn parse_receive_pack_report_status_v2_option(
6299    format: ObjectFormat,
6300    text: &str,
6301    options: &mut ReceivePackCommandStatusV2Options,
6302) -> Result<()> {
6303    if let Some(refname) = text.strip_prefix("option refname ") {
6304        if options.refname.is_some() {
6305            return Err(GitError::InvalidFormat(
6306                "receive-pack report-status-v2 has duplicate refname option".into(),
6307            ));
6308        }
6309        validate_protocol_v2_token("receive-pack status-v2 option refname", refname)?;
6310        options.refname = Some(refname.to_string());
6311    } else if let Some(old_oid) = text.strip_prefix("option old-oid ") {
6312        if options.old_oid.is_some() {
6313            return Err(GitError::InvalidFormat(
6314                "receive-pack report-status-v2 has duplicate old-oid option".into(),
6315            ));
6316        }
6317        validate_protocol_v2_token("receive-pack status-v2 option old-oid", old_oid)?;
6318        options.old_oid = Some(ObjectId::from_hex(format, old_oid)?);
6319    } else if let Some(new_oid) = text.strip_prefix("option new-oid ") {
6320        if options.new_oid.is_some() {
6321            return Err(GitError::InvalidFormat(
6322                "receive-pack report-status-v2 has duplicate new-oid option".into(),
6323            ));
6324        }
6325        validate_protocol_v2_token("receive-pack status-v2 option new-oid", new_oid)?;
6326        options.new_oid = Some(ObjectId::from_hex(format, new_oid)?);
6327    } else if text == "option forced-update" {
6328        if options.forced_update {
6329            return Err(GitError::InvalidFormat(
6330                "receive-pack report-status-v2 has duplicate forced-update option".into(),
6331            ));
6332        }
6333        options.forced_update = true;
6334    } else {
6335        return Err(GitError::InvalidFormat(format!(
6336            "unsupported receive-pack report-status-v2 option {text}"
6337        )));
6338    }
6339    Ok(())
6340}
6341
6342fn format_receive_pack_report_status_v2_options(
6343    options: &ReceivePackCommandStatusV2Options,
6344) -> Result<Vec<String>> {
6345    let mut out = Vec::new();
6346    if let Some(refname) = &options.refname {
6347        validate_protocol_v2_token("receive-pack status-v2 option refname", refname)?;
6348        out.push(format!("option refname {refname}"));
6349    }
6350    if let Some(old_oid) = &options.old_oid {
6351        out.push(format!("option old-oid {old_oid}"));
6352    }
6353    if let Some(new_oid) = &options.new_oid {
6354        out.push(format!("option new-oid {new_oid}"));
6355    }
6356    if options.forced_update {
6357        out.push("option forced-update".into());
6358    }
6359    Ok(out)
6360}
6361
6362fn validate_receive_pack_status_message(label: &str, message: &str) -> Result<()> {
6363    if message.is_empty() {
6364        return Err(GitError::InvalidFormat(format!("{label} is empty")));
6365    }
6366    if message
6367        .bytes()
6368        .any(|byte| matches!(byte, b'\n' | b'\r' | 0))
6369    {
6370        return Err(GitError::InvalidFormat(format!(
6371            "{label} contains a delimiter byte"
6372        )));
6373    }
6374    Ok(())
6375}
6376
6377fn validate_receive_pack_push_option(option: &[u8]) -> Result<()> {
6378    if option.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
6379        return Err(GitError::InvalidFormat(
6380            "receive-pack push-option contains a delimiter byte".into(),
6381        ));
6382    }
6383    Ok(())
6384}
6385
6386fn validate_protocol_error_message(message: &str) -> Result<()> {
6387    if message.is_empty() {
6388        return Err(GitError::InvalidFormat(
6389            "protocol error message is empty".into(),
6390        ));
6391    }
6392    if message
6393        .bytes()
6394        .any(|byte| matches!(byte, b'\n' | b'\r' | 0))
6395    {
6396        return Err(GitError::InvalidFormat(
6397            "protocol error message contains a delimiter byte".into(),
6398        ));
6399    }
6400    Ok(())
6401}
6402
6403fn parse_capability_token(token: &str) -> Result<Capability> {
6404    if token.is_empty() {
6405        return Err(GitError::InvalidFormat("empty capability token".into()));
6406    }
6407    let (name, value) = token
6408        .split_once('=')
6409        .map_or((token, None), |(name, value)| (name, Some(value)));
6410    validate_capability_field("capability name", name)?;
6411    if let Some(value) = value {
6412        validate_capability_field("capability value", value)?;
6413    }
6414    Ok(Capability {
6415        name: name.to_string(),
6416        value: value.map(str::to_string),
6417    })
6418}
6419
6420fn parse_protocol_v2_capability_line(payload: &[u8]) -> Result<Capability> {
6421    let payload = trim_trailing_lf(payload);
6422    if payload.is_empty() {
6423        return Err(GitError::InvalidFormat(
6424            "empty protocol v2 capability line".into(),
6425        ));
6426    }
6427    let text =
6428        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6429    let (name, value) = text
6430        .split_once('=')
6431        .map_or((text, None), |(name, value)| (name, Some(value)));
6432    validate_capability_name(name)?;
6433    if let Some(value) = value {
6434        validate_protocol_v2_capability_value(value)?;
6435    }
6436    Ok(Capability {
6437        name: name.to_string(),
6438        value: value.map(str::to_string),
6439    })
6440}
6441
6442fn parse_protocol_v2_command_line(payload: &[u8]) -> Result<String> {
6443    let payload = trim_trailing_lf(payload);
6444    let text =
6445        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6446    let Some(command) = text.strip_prefix("command=") else {
6447        return Err(GitError::InvalidFormat(
6448            "protocol v2 command request missing command prefix".into(),
6449        ));
6450    };
6451    validate_capability_name(command)?;
6452    Ok(command.to_string())
6453}
6454
6455fn parse_protocol_v2_ls_refs_line(
6456    format: ObjectFormat,
6457    payload: &[u8],
6458) -> Result<ProtocolV2LsRefsRecord> {
6459    let payload = trim_trailing_lf(payload);
6460    if payload.is_empty() {
6461        return Err(GitError::InvalidFormat(
6462            "ls-refs response line is empty".into(),
6463        ));
6464    }
6465    let text =
6466        std::str::from_utf8(payload).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6467    let mut fields = text.split(' ');
6468    let first = fields
6469        .next()
6470        .ok_or_else(|| GitError::InvalidFormat("ls-refs response line is empty".into()))?;
6471    if first == "unborn" {
6472        let name = fields
6473            .next()
6474            .ok_or_else(|| GitError::InvalidFormat("ls-refs unborn line is missing name".into()))?;
6475        validate_protocol_v2_token("ls-refs ref name", name)?;
6476        let (symref_target, attributes) = parse_protocol_v2_ls_refs_attributes(format, fields)?;
6477        return Ok(ProtocolV2LsRefsRecord::Unborn {
6478            name: name.to_string(),
6479            symref_target,
6480            attributes,
6481        });
6482    }
6483
6484    let oid = ObjectId::from_hex(format, first)?;
6485    let name = fields
6486        .next()
6487        .ok_or_else(|| GitError::InvalidFormat("ls-refs ref line is missing name".into()))?;
6488    validate_protocol_v2_token("ls-refs ref name", name)?;
6489    let (peeled, symref_target, attributes) =
6490        parse_protocol_v2_ls_refs_ref_attributes(format, fields)?;
6491    Ok(ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
6492        oid,
6493        name: name.to_string(),
6494        peeled,
6495        symref_target,
6496        attributes,
6497    }))
6498}
6499
6500fn parse_protocol_v2_ls_refs_ref_attributes<'a>(
6501    format: ObjectFormat,
6502    fields: impl Iterator<Item = &'a str>,
6503) -> Result<(Option<ObjectId>, Option<String>, Vec<String>)> {
6504    let mut peeled = None;
6505    let (symref_target, attributes) =
6506        parse_protocol_v2_ls_refs_attributes_with(format, fields, |attr| {
6507            if let Some(value) = attr.strip_prefix("peeled:") {
6508                if peeled.is_some() {
6509                    return Err(GitError::InvalidFormat(
6510                        "ls-refs response has duplicate peeled attribute".into(),
6511                    ));
6512                }
6513                peeled = Some(ObjectId::from_hex(format, value)?);
6514                return Ok(true);
6515            }
6516            Ok(false)
6517        })?;
6518    Ok((peeled, symref_target, attributes))
6519}
6520
6521fn parse_protocol_v2_ls_refs_attributes<'a>(
6522    format: ObjectFormat,
6523    fields: impl Iterator<Item = &'a str>,
6524) -> Result<(Option<String>, Vec<String>)> {
6525    parse_protocol_v2_ls_refs_attributes_with(format, fields, |attr| {
6526        if attr.starts_with("peeled:") {
6527            return Err(GitError::InvalidFormat(
6528                "ls-refs unborn line has peeled attribute".into(),
6529            ));
6530        }
6531        Ok(false)
6532    })
6533}
6534
6535fn parse_protocol_v2_ls_refs_attributes_with<'a, F>(
6536    _format: ObjectFormat,
6537    fields: impl Iterator<Item = &'a str>,
6538    mut handle_known: F,
6539) -> Result<(Option<String>, Vec<String>)>
6540where
6541    F: FnMut(&str) -> Result<bool>,
6542{
6543    let mut symref_target = None;
6544    let mut attributes = Vec::new();
6545    for attr in fields {
6546        validate_protocol_v2_token("ls-refs attribute", attr)?;
6547        if let Some(value) = attr.strip_prefix("symref-target:") {
6548            if symref_target.is_some() {
6549                return Err(GitError::InvalidFormat(
6550                    "ls-refs response has duplicate symref-target attribute".into(),
6551                ));
6552            }
6553            validate_protocol_v2_token("ls-refs symref-target", value)?;
6554            symref_target = Some(value.to_string());
6555        } else if !handle_known(attr)? {
6556            attributes.push(attr.to_string());
6557        }
6558    }
6559    Ok((symref_target, attributes))
6560}
6561
6562fn format_protocol_v2_ls_refs_record(record: &ProtocolV2LsRefsRecord) -> Result<String> {
6563    let mut out = String::new();
6564    match record {
6565        ProtocolV2LsRefsRecord::Ref(reference) => {
6566            validate_protocol_v2_token("ls-refs ref name", &reference.name)?;
6567            out.push_str(&reference.oid.to_string());
6568            out.push(' ');
6569            out.push_str(&reference.name);
6570            if let Some(peeled) = &reference.peeled {
6571                if peeled.format() != reference.oid.format() {
6572                    return Err(GitError::InvalidObjectId(
6573                        "ls-refs peeled object format does not match ref object format".into(),
6574                    ));
6575                }
6576                out.push(' ');
6577                out.push_str("peeled:");
6578                out.push_str(&peeled.to_string());
6579            }
6580            if let Some(target) = &reference.symref_target {
6581                validate_protocol_v2_token("ls-refs symref-target", target)?;
6582                out.push(' ');
6583                out.push_str("symref-target:");
6584                out.push_str(target);
6585            }
6586            append_protocol_v2_ls_refs_attributes(&mut out, &reference.attributes)?;
6587        }
6588        ProtocolV2LsRefsRecord::Unborn {
6589            name,
6590            symref_target,
6591            attributes,
6592        } => {
6593            validate_protocol_v2_token("ls-refs ref name", name)?;
6594            out.push_str("unborn ");
6595            out.push_str(name);
6596            if let Some(target) = symref_target {
6597                validate_protocol_v2_token("ls-refs symref-target", target)?;
6598                out.push(' ');
6599                out.push_str("symref-target:");
6600                out.push_str(target);
6601            }
6602            append_protocol_v2_ls_refs_attributes(&mut out, attributes)?;
6603        }
6604    }
6605    Ok(out)
6606}
6607
6608fn append_protocol_v2_ls_refs_attributes(out: &mut String, attributes: &[String]) -> Result<()> {
6609    for attr in attributes {
6610        validate_protocol_v2_token("ls-refs attribute", attr)?;
6611        if attr.starts_with("peeled:") || attr.starts_with("symref-target:") {
6612            return Err(GitError::InvalidFormat(
6613                "ls-refs generic attributes must not duplicate known attributes".into(),
6614            ));
6615        }
6616        out.push(' ');
6617        out.push_str(attr);
6618    }
6619    Ok(())
6620}
6621
6622fn parse_fetch_section_header(payload: &[u8]) -> Result<String> {
6623    let name = parse_protocol_v2_line_text("fetch response section", payload)?;
6624    validate_capability_name(name)?;
6625    Ok(name.to_string())
6626}
6627
6628fn flush_terminates_protocol_v2_response(frames: &[PktLineFrame], idx: usize) -> bool {
6629    idx + 1 == frames.len()
6630        || (idx + 2 == frames.len() && matches!(frames[idx + 1], PktLineFrame::ResponseEnd))
6631}
6632
6633fn parse_fetch_section(
6634    format: ObjectFormat,
6635    name: String,
6636    lines: Vec<Vec<u8>>,
6637) -> Result<ProtocolV2FetchResponseSection> {
6638    match name.as_str() {
6639        "acknowledgments" => lines
6640            .iter()
6641            .map(|line| parse_fetch_acknowledgment(format, line))
6642            .collect::<Result<Vec<_>>>()
6643            .map(ProtocolV2FetchResponseSection::Acknowledgments),
6644        "shallow-info" => lines
6645            .iter()
6646            .map(|line| parse_fetch_shallow_info(format, line))
6647            .collect::<Result<Vec<_>>>()
6648            .map(ProtocolV2FetchResponseSection::ShallowInfo),
6649        "wanted-refs" => lines
6650            .iter()
6651            .map(|line| parse_fetch_wanted_ref(format, line))
6652            .collect::<Result<Vec<_>>>()
6653            .map(ProtocolV2FetchResponseSection::WantedRefs),
6654        "packfile-uris" => lines
6655            .iter()
6656            .map(|line| parse_fetch_packfile_uri(format, line))
6657            .collect::<Result<Vec<_>>>()
6658            .map(ProtocolV2FetchResponseSection::PackfileUris),
6659        "packfile" => Ok(ProtocolV2FetchResponseSection::Packfile(lines)),
6660        _ => Ok(ProtocolV2FetchResponseSection::Unknown { name, lines }),
6661    }
6662}
6663
6664fn parse_fetch_acknowledgment(
6665    format: ObjectFormat,
6666    line: &[u8],
6667) -> Result<ProtocolV2FetchAcknowledgment> {
6668    let text = parse_protocol_v2_line_text("fetch acknowledgment", line)?;
6669    match text {
6670        "NAK" => Ok(ProtocolV2FetchAcknowledgment::Nak),
6671        "ready" => Ok(ProtocolV2FetchAcknowledgment::Ready),
6672        value if value.starts_with("ACK ") => Ok(ProtocolV2FetchAcknowledgment::Ack(
6673            parse_oid_argument(format, "fetch ACK", value, "ACK ")?,
6674        )),
6675        other => Err(GitError::InvalidFormat(format!(
6676            "unsupported fetch acknowledgment {other}"
6677        ))),
6678    }
6679}
6680
6681fn parse_fetch_shallow_info(
6682    format: ObjectFormat,
6683    line: &[u8],
6684) -> Result<ProtocolV2FetchShallowInfo> {
6685    let text = parse_protocol_v2_line_text("fetch shallow-info", line)?;
6686    if text.starts_with("shallow ") {
6687        return Ok(ProtocolV2FetchShallowInfo::Shallow(parse_oid_argument(
6688            format,
6689            "fetch shallow",
6690            text,
6691            "shallow ",
6692        )?));
6693    }
6694    if text.starts_with("unshallow ") {
6695        return Ok(ProtocolV2FetchShallowInfo::Unshallow(parse_oid_argument(
6696            format,
6697            "fetch unshallow",
6698            text,
6699            "unshallow ",
6700        )?));
6701    }
6702    Err(GitError::InvalidFormat(format!(
6703        "unsupported fetch shallow-info {text}"
6704    )))
6705}
6706
6707fn parse_fetch_wanted_ref(format: ObjectFormat, line: &[u8]) -> Result<ProtocolV2FetchWantedRef> {
6708    let text = parse_protocol_v2_line_text("fetch wanted-ref", line)?;
6709    let (oid, name) = text
6710        .split_once(' ')
6711        .ok_or_else(|| GitError::InvalidFormat("fetch wanted-ref is missing name".into()))?;
6712    validate_protocol_v2_token("fetch wanted-ref name", name)?;
6713    Ok(ProtocolV2FetchWantedRef {
6714        oid: ObjectId::from_hex(format, oid)?,
6715        name: name.to_string(),
6716    })
6717}
6718
6719fn parse_fetch_packfile_uri(
6720    format: ObjectFormat,
6721    line: &[u8],
6722) -> Result<ProtocolV2FetchPackfileUri> {
6723    let text = parse_protocol_v2_line_text("fetch packfile-uri", line)?;
6724    let (pack_hash, uri) = text
6725        .split_once(' ')
6726        .ok_or_else(|| GitError::InvalidFormat("fetch packfile-uri is missing uri".into()))?;
6727    validate_protocol_v2_token("fetch packfile-uri hash", pack_hash)?;
6728    validate_protocol_v2_token("fetch packfile-uri", uri)?;
6729    Ok(ProtocolV2FetchPackfileUri {
6730        pack_hash: ObjectId::from_hex(format, pack_hash)?,
6731        uri: uri.to_string(),
6732    })
6733}
6734
6735fn protocol_v2_fetch_section_name(section: &ProtocolV2FetchResponseSection) -> &str {
6736    match section {
6737        ProtocolV2FetchResponseSection::Acknowledgments(_) => "acknowledgments",
6738        ProtocolV2FetchResponseSection::ShallowInfo(_) => "shallow-info",
6739        ProtocolV2FetchResponseSection::WantedRefs(_) => "wanted-refs",
6740        ProtocolV2FetchResponseSection::PackfileUris(_) => "packfile-uris",
6741        ProtocolV2FetchResponseSection::Packfile(_) => "packfile",
6742        ProtocolV2FetchResponseSection::Unknown { name, .. } => name,
6743    }
6744}
6745
6746fn format_protocol_v2_fetch_section_lines(
6747    section: &ProtocolV2FetchResponseSection,
6748) -> Result<Vec<Vec<u8>>> {
6749    match section {
6750        ProtocolV2FetchResponseSection::Acknowledgments(acks) => acks
6751            .iter()
6752            .map(|ack| match ack {
6753                ProtocolV2FetchAcknowledgment::Nak => Ok(line_from_str("NAK")),
6754                ProtocolV2FetchAcknowledgment::Ack(oid) => Ok(line_from_str(&format!("ACK {oid}"))),
6755                ProtocolV2FetchAcknowledgment::Ready => Ok(line_from_str("ready")),
6756            })
6757            .collect(),
6758        ProtocolV2FetchResponseSection::ShallowInfo(entries) => entries
6759            .iter()
6760            .map(|entry| match entry {
6761                ProtocolV2FetchShallowInfo::Shallow(oid) => {
6762                    Ok(line_from_str(&format!("shallow {oid}")))
6763                }
6764                ProtocolV2FetchShallowInfo::Unshallow(oid) => {
6765                    Ok(line_from_str(&format!("unshallow {oid}")))
6766                }
6767            })
6768            .collect(),
6769        ProtocolV2FetchResponseSection::WantedRefs(refs) => refs
6770            .iter()
6771            .map(|wanted| {
6772                validate_protocol_v2_token("fetch wanted-ref name", &wanted.name)?;
6773                Ok(line_from_str(&format!("{} {}", wanted.oid, wanted.name)))
6774            })
6775            .collect(),
6776        ProtocolV2FetchResponseSection::PackfileUris(uris) => uris
6777            .iter()
6778            .map(|packfile_uri| {
6779                validate_protocol_v2_token("fetch packfile-uri", &packfile_uri.uri)?;
6780                Ok(line_from_str(&format!(
6781                    "{} {}",
6782                    packfile_uri.pack_hash, packfile_uri.uri
6783                )))
6784            })
6785            .collect(),
6786        ProtocolV2FetchResponseSection::Packfile(lines) => Ok(lines.clone()),
6787        ProtocolV2FetchResponseSection::Unknown { name, lines } => {
6788            validate_capability_name(name)?;
6789            for line in lines {
6790                validate_protocol_v2_line("fetch unknown section line", line)?;
6791            }
6792            Ok(lines.clone())
6793        }
6794    }
6795}
6796
6797fn parse_protocol_v2_object_info_record(
6798    format: ObjectFormat,
6799    line: &[u8],
6800) -> Result<ProtocolV2ObjectInfoRecord> {
6801    let text = parse_protocol_v2_line_text("object-info record", line)?;
6802    let mut fields = text.split(' ');
6803    let oid = fields
6804        .next()
6805        .ok_or_else(|| GitError::InvalidFormat("object-info record is missing oid".into()))?;
6806    let size = fields
6807        .next()
6808        .ok_or_else(|| GitError::InvalidFormat("object-info record is missing size".into()))?;
6809    if fields.next().is_some() {
6810        return Err(GitError::InvalidFormat(
6811            "object-info record has too many fields".into(),
6812        ));
6813    }
6814    validate_protocol_v2_token("object-info oid", oid)?;
6815    validate_protocol_v2_token("object-info size", size)?;
6816    let size = size
6817        .parse::<u64>()
6818        .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6819    Ok(ProtocolV2ObjectInfoRecord {
6820        oid: ObjectId::from_hex(format, oid)?,
6821        size,
6822    })
6823}
6824
6825fn parse_dumb_http_info_ref_record(format: ObjectFormat, line: &[u8]) -> Result<DumbHttpRefRecord> {
6826    validate_dumb_http_info_ref_line(line)?;
6827    let line = trim_trailing_lf(line);
6828    let tab = line
6829        .iter()
6830        .position(|byte| *byte == b'\t')
6831        .ok_or_else(|| GitError::InvalidFormat("dumb HTTP ref record is missing name".into()))?;
6832    let (oid, name) = (&line[..tab], &line[tab + 1..]);
6833    let oid = std::str::from_utf8(oid).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6834    validate_protocol_v2_token("dumb HTTP ref oid", oid)?;
6835    let name = std::str::from_utf8(name).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6836    let (name, peeled) = name
6837        .strip_suffix("^{}")
6838        .map_or((name, false), |name| (name, true));
6839    validate_dumb_http_ref_name(name)?;
6840    Ok(DumbHttpRefRecord {
6841        oid: ObjectId::from_hex(format, oid)?,
6842        name: name.to_string(),
6843        peeled,
6844    })
6845}
6846
6847fn parse_dumb_http_alternate(line: &[u8]) -> Result<String> {
6848    validate_dumb_http_alternate_line(line)?;
6849    let line = trim_trailing_lf(line);
6850    let alternate =
6851        std::str::from_utf8(line).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
6852    validate_dumb_http_alternate(alternate)?;
6853    Ok(alternate.to_string())
6854}
6855
6856fn parse_dumb_http_pack_record(format: ObjectFormat, line: &[u8]) -> Result<DumbHttpPackRecord> {
6857    validate_dumb_http_info_ref_line(line)?;
6858    let line = parse_protocol_v2_line_text("dumb HTTP pack record", line)?;
6859    let pack_name = line
6860        .strip_prefix("P ")
6861        .ok_or_else(|| GitError::InvalidFormat("dumb HTTP pack record must start with P".into()))?;
6862    let hash = pack_name
6863        .strip_prefix("pack-")
6864        .and_then(|value| value.strip_suffix(".pack"))
6865        .ok_or_else(|| GitError::InvalidFormat("invalid dumb HTTP pack name".into()))?;
6866    validate_protocol_v2_token("dumb HTTP pack hash", hash)?;
6867    Ok(DumbHttpPackRecord {
6868        hash: ObjectId::from_hex(format, hash)?,
6869    })
6870}
6871
6872fn encode_protocol_v2_capability(capability: &Capability) -> Result<Vec<u8>> {
6873    validate_capability_name(&capability.name)?;
6874    let mut out = capability.name.as_bytes().to_vec();
6875    if let Some(value) = &capability.value {
6876        validate_protocol_v2_capability_value(value)?;
6877        out.push(b'=');
6878        out.extend_from_slice(value.as_bytes());
6879    }
6880    Ok(out)
6881}
6882
6883fn validate_capability_field(label: &str, value: &str) -> Result<()> {
6884    if value.is_empty() {
6885        return Err(GitError::InvalidFormat(format!("{label} is empty")));
6886    }
6887    if value
6888        .bytes()
6889        .any(|byte| matches!(byte, b' ' | b'\n' | b'\r' | b'\t' | 0))
6890    {
6891        return Err(GitError::InvalidFormat(format!(
6892            "{label} contains a delimiter byte"
6893        )));
6894    }
6895    Ok(())
6896}
6897
6898fn validate_capability_name(value: &str) -> Result<()> {
6899    validate_capability_field("capability name", value)?;
6900    if value.bytes().any(|byte| byte == b'=') {
6901        return Err(GitError::InvalidFormat(
6902            "capability name contains a delimiter byte".into(),
6903        ));
6904    }
6905    Ok(())
6906}
6907
6908fn validate_protocol_v2_capability_value(value: &str) -> Result<()> {
6909    if value.is_empty() {
6910        return Err(GitError::InvalidFormat(
6911            "protocol v2 capability value is empty".into(),
6912        ));
6913    }
6914    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6915        return Err(GitError::InvalidFormat(
6916            "protocol v2 capability value contains a delimiter byte".into(),
6917        ));
6918    }
6919    Ok(())
6920}
6921
6922fn validate_protocol_v2_argument(value: &[u8]) -> Result<()> {
6923    if value.is_empty() {
6924        return Err(GitError::InvalidFormat(
6925            "protocol v2 command argument is empty".into(),
6926        ));
6927    }
6928    if value.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
6929        return Err(GitError::InvalidFormat(
6930            "protocol v2 command argument contains a delimiter byte".into(),
6931        ));
6932    }
6933    Ok(())
6934}
6935
6936fn validate_upload_archive_argument(value: &str) -> Result<()> {
6937    if value.is_empty() {
6938        return Err(GitError::InvalidFormat(
6939            "upload-archive argument is empty".into(),
6940        ));
6941    }
6942    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6943        return Err(GitError::InvalidFormat(
6944            "upload-archive argument contains a delimiter byte".into(),
6945        ));
6946    }
6947    Ok(())
6948}
6949
6950fn validate_upload_archive_status_message(value: &str) -> Result<()> {
6951    if value.is_empty() {
6952        return Err(GitError::InvalidFormat(
6953            "upload-archive status message is empty".into(),
6954        ));
6955    }
6956    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6957        return Err(GitError::InvalidFormat(
6958            "upload-archive status message contains a delimiter byte".into(),
6959        ));
6960    }
6961    Ok(())
6962}
6963
6964fn non_empty(value: &str) -> Option<&str> {
6965    (!value.is_empty()).then_some(value)
6966}
6967
6968fn validate_refspec_value(value: &str) -> Result<()> {
6969    if value.is_empty() {
6970        return Err(GitError::InvalidFormat("refspec is empty".into()));
6971    }
6972    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
6973        return Err(GitError::InvalidFormat(
6974            "refspec contains a delimiter byte".into(),
6975        ));
6976    }
6977    Ok(())
6978}
6979
6980fn validate_refspec_endpoint(label: &str, value: &str) -> Result<()> {
6981    if value.is_empty() {
6982        return Err(GitError::InvalidFormat(format!("{label} is empty")));
6983    }
6984    if value
6985        .bytes()
6986        .any(|byte| matches!(byte, b':' | b' ' | b'\t' | b'\n' | b'\r' | 0))
6987    {
6988        return Err(GitError::InvalidFormat(format!(
6989            "{label} contains a delimiter byte"
6990        )));
6991    }
6992    Ok(())
6993}
6994
6995fn count_refspec_wildcards(value: &str) -> usize {
6996    value.bytes().filter(|byte| *byte == b'*').count()
6997}
6998
6999fn validate_refspec_shape(refspec: &RefSpec) -> Result<()> {
7000    if refspec.force && refspec.negative {
7001        return Err(GitError::InvalidFormat(
7002            "negative refspec must not be forced".into(),
7003        ));
7004    }
7005    if refspec.negative && refspec.dst.is_some() {
7006        return Err(GitError::InvalidFormat(
7007            "negative refspec must not have a destination".into(),
7008        ));
7009    }
7010    if refspec.negative && refspec.src.is_none() {
7011        return Err(GitError::InvalidFormat(
7012            "negative refspec is missing a source".into(),
7013        ));
7014    }
7015    if refspec.src.is_none() && refspec.dst.is_none() && refspec.negative {
7016        return Err(GitError::InvalidFormat(
7017            "refspec must include a source or destination".into(),
7018        ));
7019    }
7020    if let Some(src) = &refspec.src {
7021        validate_refspec_endpoint("refspec source", src)?;
7022    }
7023    if let Some(dst) = &refspec.dst {
7024        validate_refspec_endpoint("refspec destination", dst)?;
7025    }
7026    let src_pattern_count = refspec
7027        .src
7028        .as_deref()
7029        .map(count_refspec_wildcards)
7030        .unwrap_or(0);
7031    let dst_pattern_count = refspec
7032        .dst
7033        .as_deref()
7034        .map(count_refspec_wildcards)
7035        .unwrap_or(0);
7036    if src_pattern_count > 1 || dst_pattern_count > 1 {
7037        return Err(GitError::InvalidFormat(
7038            "refspec endpoint has too many wildcards".into(),
7039        ));
7040    }
7041    if refspec.dst.is_some() && (src_pattern_count == 1) != (dst_pattern_count == 1) {
7042        return Err(GitError::InvalidFormat(
7043            "refspec wildcard must appear in both source and destination".into(),
7044        ));
7045    }
7046    if refspec.pattern != (src_pattern_count == 1 || dst_pattern_count == 1) {
7047        return Err(GitError::InvalidFormat(
7048            "refspec pattern flag does not match endpoints".into(),
7049        ));
7050    }
7051    Ok(())
7052}
7053
7054fn parse_fetch_head_record(format: ObjectFormat, line: &[u8]) -> Result<FetchHeadRecord> {
7055    validate_fetch_head_line(line)?;
7056    let line = trim_trailing_lf(line);
7057    let mut fields = line.splitn(3, |byte| *byte == b'\t');
7058    let oid = fields
7059        .next()
7060        .ok_or_else(|| GitError::InvalidFormat("FETCH_HEAD record is missing oid".into()))?;
7061    let merge_marker = fields.next().ok_or_else(|| {
7062        GitError::InvalidFormat("FETCH_HEAD record is missing merge marker".into())
7063    })?;
7064    let description = fields.next().ok_or_else(|| {
7065        GitError::InvalidFormat("FETCH_HEAD record is missing description".into())
7066    })?;
7067    let oid = std::str::from_utf8(oid).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
7068    validate_protocol_v2_token("FETCH_HEAD oid", oid)?;
7069    let not_for_merge = match merge_marker {
7070        b"" => false,
7071        b"not-for-merge" => true,
7072        _ => {
7073            return Err(GitError::InvalidFormat(
7074                "FETCH_HEAD record has invalid merge marker".into(),
7075            ));
7076        }
7077    };
7078    let description =
7079        std::str::from_utf8(description).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
7080    validate_fetch_head_description_field(description)?;
7081    Ok(FetchHeadRecord {
7082        oid: ObjectId::from_hex(format, oid)?,
7083        not_for_merge,
7084        description: description.to_string(),
7085    })
7086}
7087
7088fn validate_fetch_head_line(value: &[u8]) -> Result<()> {
7089    if value.is_empty() {
7090        return Err(GitError::InvalidFormat("FETCH_HEAD record is empty".into()));
7091    }
7092    if !value.ends_with(b"\n") {
7093        return Err(GitError::InvalidFormat(
7094            "FETCH_HEAD record missing LF".into(),
7095        ));
7096    }
7097    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7098        return Err(GitError::InvalidFormat(
7099            "FETCH_HEAD record contains a delimiter byte".into(),
7100        ));
7101    }
7102    Ok(())
7103}
7104
7105fn validate_fetch_head_description_field(value: &str) -> Result<()> {
7106    if value.is_empty() {
7107        return Err(GitError::InvalidFormat(
7108            "FETCH_HEAD description is empty".into(),
7109        ));
7110    }
7111    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
7112        return Err(GitError::InvalidFormat(
7113            "FETCH_HEAD description contains a delimiter byte".into(),
7114        ));
7115    }
7116    Ok(())
7117}
7118
7119fn refspec_is_excluded(negative: &[&RefSpec], source: &str) -> Result<bool> {
7120    for refspec in negative {
7121        if refspec_matches_source(refspec, source)? {
7122            return Ok(true);
7123        }
7124    }
7125    Ok(false)
7126}
7127
7128fn zero_object_id(format: ObjectFormat) -> Result<ObjectId> {
7129    Ok(ObjectId::null(format))
7130}
7131
7132fn local_ref<'a>(refs: &'a [PushSourceRef], name: &str) -> Option<&'a PushSourceRef> {
7133    refs.iter().find(|reference| reference.name == name)
7134}
7135
7136fn remote_ref<'a>(refs: &'a [RefAdvertisement], name: &str) -> Option<&'a RefAdvertisement> {
7137    refs.iter().find(|reference| reference.name == name)
7138}
7139
7140fn validate_push_source_ref(format: ObjectFormat, reference: &PushSourceRef) -> Result<()> {
7141    if reference.oid.format() != format {
7142        return Err(GitError::InvalidObjectId(
7143            "push source ref object format does not match repository".into(),
7144        ));
7145    }
7146    validate_refspec_endpoint("push source ref name", &reference.name)
7147}
7148
7149fn require_receive_pack_feature(advertised: bool, name: &str) -> Result<()> {
7150    if advertised {
7151        Ok(())
7152    } else {
7153        Err(GitError::InvalidFormat(format!(
7154            "receive-pack feature {name} was not advertised"
7155        )))
7156    }
7157}
7158
7159fn validate_smart_http_service(service: GitService) -> Result<()> {
7160    match service {
7161        GitService::UploadPack | GitService::ReceivePack => Ok(()),
7162        GitService::UploadArchive => Err(GitError::InvalidFormat(
7163            "smart HTTP only supports upload-pack and receive-pack services".into(),
7164        )),
7165    }
7166}
7167
7168fn normalize_http_repository_path(path: &str) -> Result<String> {
7169    if path.is_empty() {
7170        return Err(GitError::InvalidFormat(
7171            "smart HTTP repository path is empty".into(),
7172        ));
7173    }
7174    if !path.starts_with('/') {
7175        return Err(GitError::InvalidFormat(
7176            "smart HTTP repository path must start with /".into(),
7177        ));
7178    }
7179    if path
7180        .bytes()
7181        .any(|byte| matches!(byte, b'\n' | b'\r' | 0 | b'?' | b'#'))
7182    {
7183        return Err(GitError::InvalidFormat(
7184            "smart HTTP repository path contains a delimiter byte".into(),
7185        ));
7186    }
7187    let normalized = path.trim_end_matches('/');
7188    Ok(if normalized.is_empty() {
7189        "/".into()
7190    } else {
7191        normalized.to_string()
7192    })
7193}
7194
7195fn dumb_http_pack_resource_path(
7196    repository_path: &str,
7197    hash: &ObjectId,
7198    suffix: &str,
7199) -> Result<String> {
7200    let repository_path = normalize_http_repository_path(repository_path)?;
7201    Ok(format!(
7202        "{repository_path}/objects/pack/pack-{hash}.{suffix}"
7203    ))
7204}
7205
7206fn parse_smart_http_content_type(value: &str, suffix: &str) -> Result<GitService> {
7207    let value = value.trim();
7208    if value.is_empty() {
7209        return Err(GitError::InvalidFormat(
7210            "smart HTTP content type is empty".into(),
7211        ));
7212    }
7213    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
7214        return Err(GitError::InvalidFormat(
7215            "smart HTTP content type contains a delimiter byte".into(),
7216        ));
7217    }
7218    let value = value.to_ascii_lowercase();
7219    let service = value
7220        .strip_prefix("application/x-")
7221        .and_then(|value| value.strip_suffix(suffix))
7222        .ok_or_else(|| GitError::InvalidFormat("invalid smart HTTP content type".into()))?;
7223    let service = parse_git_service(service)?;
7224    validate_smart_http_service(service)?;
7225    Ok(service)
7226}
7227
7228fn validate_dumb_http_info_ref_line(value: &[u8]) -> Result<()> {
7229    if value.is_empty() {
7230        return Err(GitError::InvalidFormat(
7231            "dumb HTTP ref record is empty".into(),
7232        ));
7233    }
7234    if !value.ends_with(b"\n") {
7235        return Err(GitError::InvalidFormat(
7236            "dumb HTTP ref record missing LF".into(),
7237        ));
7238    }
7239    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7240        return Err(GitError::InvalidFormat(
7241            "dumb HTTP ref record contains a delimiter byte".into(),
7242        ));
7243    }
7244    Ok(())
7245}
7246
7247fn validate_dumb_http_ref_name(value: &str) -> Result<()> {
7248    validate_protocol_v2_token("dumb HTTP ref name", value)?;
7249    if value.ends_with("^{}") {
7250        return Err(GitError::InvalidFormat(
7251            "dumb HTTP ref name must not include peeled suffix".into(),
7252        ));
7253    }
7254    Ok(())
7255}
7256
7257fn validate_dumb_http_alternate_line(value: &[u8]) -> Result<()> {
7258    if value.is_empty() {
7259        return Err(GitError::InvalidFormat(
7260            "dumb HTTP alternate is empty".into(),
7261        ));
7262    }
7263    if !value.ends_with(b"\n") {
7264        return Err(GitError::InvalidFormat(
7265            "dumb HTTP alternate missing LF".into(),
7266        ));
7267    }
7268    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7269        return Err(GitError::InvalidFormat(
7270            "dumb HTTP alternate contains a delimiter byte".into(),
7271        ));
7272    }
7273    Ok(())
7274}
7275
7276fn validate_dumb_http_alternate(value: &str) -> Result<()> {
7277    if value.is_empty() {
7278        return Err(GitError::InvalidFormat(
7279            "dumb HTTP alternate is empty".into(),
7280        ));
7281    }
7282    if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
7283        return Err(GitError::InvalidFormat(
7284            "dumb HTTP alternate contains a delimiter byte".into(),
7285        ));
7286    }
7287    Ok(())
7288}
7289
7290fn validate_protocol_v2_token(label: &str, value: &str) -> Result<()> {
7291    if value.is_empty() {
7292        return Err(GitError::InvalidFormat(format!("{label} is empty")));
7293    }
7294    if value
7295        .bytes()
7296        .any(|byte| matches!(byte, b' ' | b'\n' | b'\r' | 0))
7297    {
7298        return Err(GitError::InvalidFormat(format!(
7299            "{label} contains a delimiter byte"
7300        )));
7301    }
7302    Ok(())
7303}
7304
7305fn validate_protocol_v2_line(label: &str, value: &[u8]) -> Result<()> {
7306    if value.is_empty() {
7307        return Err(GitError::InvalidFormat(format!("{label} is empty")));
7308    }
7309    if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
7310        return Err(GitError::InvalidFormat(format!(
7311            "{label} contains a delimiter byte"
7312        )));
7313    }
7314    Ok(())
7315}
7316
7317fn parse_protocol_v2_line_text<'a>(label: &str, value: &'a [u8]) -> Result<&'a str> {
7318    validate_protocol_v2_line(label, value)?;
7319    let value = trim_trailing_lf(value);
7320    if value.is_empty() {
7321        return Err(GitError::InvalidFormat(format!("{label} is empty")));
7322    }
7323    if value.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
7324        return Err(GitError::InvalidFormat(format!(
7325            "{label} contains a delimiter byte"
7326        )));
7327    }
7328    std::str::from_utf8(value).map_err(|err| GitError::InvalidFormat(err.to_string()))
7329}
7330
7331fn parse_oid_argument(
7332    format: ObjectFormat,
7333    label: &str,
7334    value: &str,
7335    prefix: &str,
7336) -> Result<ObjectId> {
7337    let oid = value
7338        .strip_prefix(prefix)
7339        .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
7340    validate_protocol_v2_token(label, oid)?;
7341    ObjectId::from_hex(format, oid)
7342}
7343
7344fn parse_u32_argument(label: &str, value: &str, prefix: &str) -> Result<u32> {
7345    let number = value
7346        .strip_prefix(prefix)
7347        .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
7348    validate_protocol_v2_token(label, number)?;
7349    let parsed = number
7350        .parse::<u32>()
7351        .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
7352    if parsed == 0 {
7353        return Err(GitError::InvalidFormat(format!("{label} must be positive")));
7354    }
7355    Ok(parsed)
7356}
7357
7358fn parse_u64_argument(label: &str, value: &str, prefix: &str) -> Result<u64> {
7359    let number = value
7360        .strip_prefix(prefix)
7361        .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
7362    validate_protocol_v2_token(label, number)?;
7363    number
7364        .parse::<u64>()
7365        .map_err(|err| GitError::InvalidFormat(err.to_string()))
7366}
7367
7368fn line(mut payload: Vec<u8>) -> Vec<u8> {
7369    payload.push(b'\n');
7370    payload
7371}
7372
7373fn line_from_str(payload: &str) -> Vec<u8> {
7374    line(payload.as_bytes().to_vec())
7375}
7376
7377fn trim_trailing_lf(input: &[u8]) -> &[u8] {
7378    input.strip_suffix(b"\n").unwrap_or(input)
7379}
7380
7381#[cfg(test)]
7382mod tests {
7383    use super::*;
7384
7385    #[test]
7386    fn pkt_line_frame_encodes_data_and_control_frames() {
7387        assert_eq!(PktLine(b"hello\n".to_vec()).encode(), b"000ahello\n");
7388        assert_eq!(
7389            PktLineFrame::data(b"hello\n".to_vec())
7390                .expect("test operation should succeed")
7391                .encode(),
7392            b"000ahello\n"
7393        );
7394        assert_eq!(PktLineFrame::Flush.encode(), b"0000");
7395        assert_eq!(PktLineFrame::Delimiter.encode(), b"0001");
7396        assert_eq!(PktLineFrame::ResponseEnd.encode(), b"0002");
7397        assert_eq!(
7398            PktLineFrame::data(b"hello\n".to_vec())
7399                .expect("test operation should succeed")
7400                .try_encode()
7401                .expect("test operation should succeed"),
7402            b"000ahello\n"
7403        );
7404    }
7405
7406    #[test]
7407    fn pkt_line_frame_parses_data_and_control_frames() {
7408        assert_eq!(
7409            PktLineFrame::parse(b"000ahello\n").expect("test operation should succeed"),
7410            (PktLineFrame::Data(b"hello\n".to_vec()), 10)
7411        );
7412        assert_eq!(
7413            PktLineFrame::parse(b"0000").expect("test operation should succeed"),
7414            (PktLineFrame::Flush, 4)
7415        );
7416        assert_eq!(
7417            PktLineFrame::parse(b"0001").expect("test operation should succeed"),
7418            (PktLineFrame::Delimiter, 4)
7419        );
7420        assert_eq!(
7421            PktLineFrame::parse(b"0002").expect("test operation should succeed"),
7422            (PktLineFrame::ResponseEnd, 4)
7423        );
7424    }
7425
7426    #[test]
7427    fn pkt_line_stream_parses_multiple_frames() {
7428        let frames = parse_pkt_line_stream(b"000eversion 2\n00010009done\n0000")
7429            .expect("test operation should succeed");
7430        assert_eq!(
7431            frames,
7432            vec![
7433                PktLineFrame::Data(b"version 2\n".to_vec()),
7434                PktLineFrame::Delimiter,
7435                PktLineFrame::Data(b"done\n".to_vec()),
7436                PktLineFrame::Flush,
7437            ]
7438        );
7439    }
7440
7441    #[test]
7442    fn pkt_line_stream_reads_and_writes_incremental_io() {
7443        let frames = vec![
7444            PktLineFrame::Data(b"version 2\n".to_vec()),
7445            PktLineFrame::Delimiter,
7446            PktLineFrame::Data(b"done\n".to_vec()),
7447            PktLineFrame::Flush,
7448        ];
7449        let mut encoded = Vec::new();
7450        write_pkt_line_frames(&mut encoded, &frames).expect("test operation should succeed");
7451        assert_eq!(encoded, b"000eversion 2\n00010009done\n0000");
7452        assert_eq!(
7453            read_pkt_line_frames(&mut encoded.as_slice()).expect("test operation should succeed"),
7454            frames
7455        );
7456
7457        let mut empty: &[u8] = b"";
7458        assert_eq!(
7459            read_pkt_line_frame(&mut empty).expect("test operation should succeed"),
7460            None
7461        );
7462    }
7463
7464    #[test]
7465    fn pkt_line_stream_reads_until_control_packets() {
7466        let input = b"000eversion 2\n0000trailing";
7467        let frames = read_pkt_line_frames_until_flush(&mut &input[..])
7468            .expect("test operation should succeed");
7469        assert_eq!(
7470            frames,
7471            vec![
7472                PktLineFrame::Data(b"version 2\n".to_vec()),
7473                PktLineFrame::Flush,
7474            ]
7475        );
7476
7477        let input = b"0009done\n0002next";
7478        let frames = read_pkt_line_frames_until_response_end(&mut &input[..])
7479            .expect("test operation should succeed");
7480        assert_eq!(
7481            frames,
7482            vec![
7483                PktLineFrame::Data(b"done\n".to_vec()),
7484                PktLineFrame::ResponseEnd,
7485            ]
7486        );
7487        assert!(read_pkt_line_frames_until_flush(&mut &b"0009done\n"[..]).is_err());
7488    }
7489
7490    #[test]
7491    fn pkt_line_rejects_invalid_lengths() {
7492        assert!(PktLineFrame::parse(b"000").is_err());
7493        assert!(PktLineFrame::parse(b"0003").is_err());
7494        assert!(PktLineFrame::parse(b"000ahello").is_err());
7495        assert!(PktLineFrame::parse(b"zzzz").is_err());
7496        assert!(read_pkt_line_frame(&mut &b"000"[..]).is_err());
7497        assert!(read_pkt_line_frame(&mut &b"0003"[..]).is_err());
7498    }
7499
7500    #[test]
7501    fn pkt_line_rejects_oversized_data() {
7502        let payload = vec![b'x'; PKT_LINE_MAX_PAYLOAD_LEN + 1];
7503        assert!(PktLineFrame::data(payload.clone()).is_err());
7504        assert!(PktLine(payload.clone()).try_encode().is_err());
7505        assert!(PktLineFrame::Data(payload.clone()).try_encode().is_err());
7506        assert!(write_pkt_line_frame(&mut Vec::new(), &PktLineFrame::Data(payload)).is_err());
7507        assert!(PktLineFrame::parse(b"fff1").is_err());
7508    }
7509
7510    #[test]
7511    fn protocol_error_lines_parse_encode_and_stream() {
7512        let error = parse_error_line(b"ERR remote rejected request\n")
7513            .expect("test operation should succeed");
7514        assert_eq!(
7515            error,
7516            ProtocolErrorLine {
7517                message: "remote rejected request".into(),
7518            }
7519        );
7520        assert_eq!(
7521            encode_error_line(&error).expect("test operation should succeed"),
7522            b"ERR remote rejected request\n"
7523        );
7524        assert_eq!(
7525            parse_error_frame(&PktLineFrame::Data(
7526                b"ERR remote rejected request\n".to_vec()
7527            ))
7528            .expect("test operation should succeed"),
7529            Some(error.clone())
7530        );
7531        assert_eq!(
7532            parse_error_frame(&PktLineFrame::Data(b"NAK\n".to_vec()))
7533                .expect("test operation should succeed"),
7534            None
7535        );
7536
7537        let mut encoded = Vec::new();
7538        write_error_line(&mut encoded, &error).expect("test operation should succeed");
7539        encoded.extend_from_slice(b"tail");
7540        let mut input = encoded.as_slice();
7541        assert_eq!(
7542            read_error_line(&mut input).expect("test operation should succeed"),
7543            error
7544        );
7545        assert_eq!(input, b"tail");
7546    }
7547
7548    #[test]
7549    fn protocol_error_lines_reject_malformed_messages() {
7550        assert!(parse_error_line(b"ERR\n").is_err());
7551        assert!(parse_error_line(b"ERR \n").is_err());
7552        assert!(parse_error_line(b"ERR bad\0message\n").is_err());
7553        assert!(parse_error_line(b"NAK\n").is_err());
7554        assert!(
7555            encode_error_line(&ProtocolErrorLine {
7556                message: "bad\nmessage".into(),
7557            })
7558            .is_err()
7559        );
7560        assert!(read_error_line(&mut &b"0000"[..]).is_err());
7561    }
7562
7563    #[test]
7564    fn refspec_parser_handles_fetch_push_and_negative_forms() {
7565        assert_eq!(
7566            parse_refspec("+refs/heads/*:refs/remotes/origin/*")
7567                .expect("test operation should succeed"),
7568            RefSpec {
7569                force: true,
7570                negative: false,
7571                src: Some("refs/heads/*".into()),
7572                dst: Some("refs/remotes/origin/*".into()),
7573                pattern: true,
7574            }
7575        );
7576        assert_eq!(
7577            parse_refspec("refs/heads/main").expect("test operation should succeed"),
7578            RefSpec {
7579                force: false,
7580                negative: false,
7581                src: Some("refs/heads/main".into()),
7582                dst: None,
7583                pattern: false,
7584            }
7585        );
7586        assert_eq!(
7587            parse_refspec(":refs/heads/topic").expect("test operation should succeed"),
7588            RefSpec {
7589                force: false,
7590                negative: false,
7591                src: None,
7592                dst: Some("refs/heads/topic".into()),
7593                pattern: false,
7594            }
7595        );
7596        assert_eq!(
7597            parse_refspec(":").expect("test operation should succeed"),
7598            RefSpec {
7599                force: false,
7600                negative: false,
7601                src: None,
7602                dst: None,
7603                pattern: false,
7604            }
7605        );
7606        assert_eq!(
7607            parse_refspec("^refs/tags/private/*").expect("test operation should succeed"),
7608            RefSpec {
7609                force: false,
7610                negative: true,
7611                src: Some("refs/tags/private/*".into()),
7612                dst: None,
7613                pattern: true,
7614            }
7615        );
7616    }
7617
7618    #[test]
7619    fn refspec_encode_and_map_sources() {
7620        let pattern = parse_refspec("+refs/heads/*:refs/remotes/origin/*")
7621            .expect("test operation should succeed");
7622        assert_eq!(
7623            encode_refspec(&pattern).expect("test operation should succeed"),
7624            "+refs/heads/*:refs/remotes/origin/*"
7625        );
7626        assert!(
7627            refspec_matches_source(&pattern, "refs/heads/main")
7628                .expect("test operation should succeed")
7629        );
7630        assert_eq!(
7631            refspec_map_source(&pattern, "refs/heads/main").expect("test operation should succeed"),
7632            Some("refs/remotes/origin/main".into())
7633        );
7634        assert_eq!(
7635            refspec_map_source(&pattern, "refs/tags/v1").expect("test operation should succeed"),
7636            None
7637        );
7638
7639        let direct = parse_refspec("HEAD:refs/heads/main").expect("test operation should succeed");
7640        assert_eq!(
7641            encode_refspec(&direct).expect("test operation should succeed"),
7642            "HEAD:refs/heads/main"
7643        );
7644        assert_eq!(
7645            refspec_map_source(&direct, "HEAD").expect("test operation should succeed"),
7646            Some("refs/heads/main".into())
7647        );
7648
7649        let delete = parse_refspec(":refs/heads/old").expect("test operation should succeed");
7650        assert_eq!(
7651            encode_refspec(&delete).expect("test operation should succeed"),
7652            ":refs/heads/old"
7653        );
7654        assert_eq!(
7655            refspec_map_source(&delete, "HEAD").expect("test operation should succeed"),
7656            None
7657        );
7658
7659        let matching = parse_refspec(":").expect("test operation should succeed");
7660        assert_eq!(
7661            encode_refspec(&matching).expect("test operation should succeed"),
7662            ":"
7663        );
7664    }
7665
7666    #[test]
7667    fn refspec_parser_rejects_malformed_values() {
7668        assert!(parse_refspec("").is_err());
7669        assert!(parse_refspec("+^refs/heads/main").is_err());
7670        assert!(parse_refspec("^refs/heads/main:refs/remotes/origin/main").is_err());
7671        assert!(parse_refspec("refs/heads/*:refs/remotes/origin/main").is_err());
7672        assert!(parse_refspec("refs/heads/**:refs/remotes/origin/*").is_err());
7673        assert!(parse_refspec("refs/heads/main:refs/remotes/origin/main:extra").is_err());
7674        assert!(parse_refspec("refs/heads/main\n").is_err());
7675        assert!(
7676            encode_refspec(&RefSpec {
7677                force: false,
7678                negative: false,
7679                src: Some("refs/heads/*".into()),
7680                dst: Some("refs/remotes/origin/main".into()),
7681                pattern: true,
7682            })
7683            .is_err()
7684        );
7685    }
7686
7687    #[test]
7688    fn fetch_head_records_parse_encode_and_describe_refs() {
7689        let first = ObjectId::from_hex(
7690            ObjectFormat::Sha1,
7691            "1111111111111111111111111111111111111111",
7692        )
7693        .expect("test operation should succeed");
7694        let second = ObjectId::from_hex(
7695            ObjectFormat::Sha1,
7696            "2222222222222222222222222222222222222222",
7697        )
7698        .expect("test operation should succeed");
7699        let input = b"1111111111111111111111111111111111111111\t\tbranch 'main' of ../bundle.bdl\n2222222222222222222222222222222222222222\tnot-for-merge\ttag 'v1' of ../bundle.bdl\n";
7700        let records =
7701            parse_fetch_head(ObjectFormat::Sha1, input).expect("test operation should succeed");
7702        assert_eq!(
7703            records,
7704            vec![
7705                FetchHeadRecord {
7706                    oid: first,
7707                    not_for_merge: false,
7708                    description: "branch 'main' of ../bundle.bdl".into(),
7709                },
7710                FetchHeadRecord {
7711                    oid: second,
7712                    not_for_merge: true,
7713                    description: "tag 'v1' of ../bundle.bdl".into(),
7714                },
7715            ]
7716        );
7717        assert_eq!(
7718            encode_fetch_head(&records).expect("test operation should succeed"),
7719            input
7720        );
7721        assert_eq!(
7722            parse_fetch_head(ObjectFormat::Sha1, b"").expect("test operation should succeed"),
7723            Vec::<FetchHeadRecord>::new()
7724        );
7725        assert_eq!(
7726            fetch_head_remote_description("refs/heads/main", "../bundle.bdl")
7727                .expect("test operation should succeed"),
7728            "branch 'main' of ../bundle.bdl"
7729        );
7730        assert_eq!(
7731            fetch_head_remote_description("refs/tags/v1", "../bundle.bdl")
7732                .expect("test operation should succeed"),
7733            "tag 'v1' of ../bundle.bdl"
7734        );
7735        // A bare `HEAD` fetch records just the URL — git emits an empty note.
7736        assert_eq!(
7737            fetch_head_remote_description("HEAD", "../bundle.bdl")
7738                .expect("test operation should succeed"),
7739            "../bundle.bdl"
7740        );
7741    }
7742
7743    #[test]
7744    fn fetch_head_records_streams_round_trip() {
7745        let records = vec![FetchHeadRecord {
7746            oid: ObjectId::from_hex(
7747                ObjectFormat::Sha1,
7748                "1111111111111111111111111111111111111111",
7749            )
7750            .expect("test operation should succeed"),
7751            not_for_merge: false,
7752            description: "branch 'main' of ../bundle.bdl".into(),
7753        }];
7754        let mut encoded = Vec::new();
7755        write_fetch_head(&mut encoded, &records).expect("test operation should succeed");
7756        let mut input = encoded.as_slice();
7757        assert_eq!(
7758            read_fetch_head(ObjectFormat::Sha1, &mut input).expect("test operation should succeed"),
7759            records
7760        );
7761        assert!(input.is_empty());
7762    }
7763
7764    #[test]
7765    fn fetch_head_records_reject_malformed_lines() {
7766        assert!(
7767            parse_fetch_head(
7768                ObjectFormat::Sha1,
7769                b"1111111111111111111111111111111111111111\t\tbranch 'main'"
7770            )
7771            .is_err()
7772        );
7773        assert!(
7774            parse_fetch_head(
7775                ObjectFormat::Sha1,
7776                b"1111111111111111111111111111111111111111\tfor-merge\tbranch 'main'\n"
7777            )
7778            .is_err()
7779        );
7780        assert!(parse_fetch_head(ObjectFormat::Sha1, b"not-a-hash\t\tbranch 'main'\n").is_err());
7781        assert!(
7782            encode_fetch_head(&[FetchHeadRecord {
7783                oid: ObjectId::from_hex(
7784                    ObjectFormat::Sha1,
7785                    "1111111111111111111111111111111111111111"
7786                )
7787                .expect("test operation should succeed"),
7788                not_for_merge: false,
7789                description: "bad\ndescription".into(),
7790            }])
7791            .is_err()
7792        );
7793    }
7794
7795    #[test]
7796    fn fetch_planner_maps_direct_pattern_and_negative_refspecs() {
7797        let main = ObjectId::from_hex(
7798            ObjectFormat::Sha1,
7799            "1111111111111111111111111111111111111111",
7800        )
7801        .expect("test operation should succeed");
7802        let next = ObjectId::from_hex(
7803            ObjectFormat::Sha1,
7804            "2222222222222222222222222222222222222222",
7805        )
7806        .expect("test operation should succeed");
7807        let refs = vec![
7808            RefAdvertisement {
7809                oid: main.clone(),
7810                name: "refs/heads/main".into(),
7811                capabilities: Vec::new(),
7812            },
7813            RefAdvertisement {
7814                oid: next.clone(),
7815                name: "refs/heads/tmp".into(),
7816                capabilities: Vec::new(),
7817            },
7818        ];
7819        let refspecs = vec![
7820            parse_refspec("+refs/heads/*:refs/remotes/origin/*")
7821                .expect("test operation should succeed"),
7822            parse_refspec("^refs/heads/tmp").expect("test operation should succeed"),
7823        ];
7824        assert_eq!(
7825            plan_fetch_ref_updates(&refs, &refspecs, false).expect("test operation should succeed"),
7826            vec![FetchRefUpdate {
7827                src: "refs/heads/main".into(),
7828                dst: Some("refs/remotes/origin/main".into()),
7829                oid: main,
7830                not_for_merge: false,
7831                force: true,
7832            }]
7833        );
7834    }
7835
7836    #[test]
7837    fn fetch_planner_autofollows_tags_and_builds_fetch_head_records() {
7838        let commit = ObjectId::from_hex(
7839            ObjectFormat::Sha1,
7840            "1111111111111111111111111111111111111111",
7841        )
7842        .expect("test operation should succeed");
7843        let refs = vec![
7844            RefAdvertisement {
7845                oid: commit.clone(),
7846                name: "refs/heads/main".into(),
7847                capabilities: Vec::new(),
7848            },
7849            RefAdvertisement {
7850                oid: commit.clone(),
7851                name: "refs/tags/v1".into(),
7852                capabilities: Vec::new(),
7853            },
7854        ];
7855        let refspecs = vec![
7856            parse_refspec("refs/heads/main:refs/heads/main")
7857                .expect("test operation should succeed"),
7858        ];
7859        let updates =
7860            plan_fetch_ref_updates(&refs, &refspecs, true).expect("test operation should succeed");
7861        assert_eq!(
7862            updates,
7863            vec![
7864                FetchRefUpdate {
7865                    src: "refs/heads/main".into(),
7866                    dst: Some("refs/heads/main".into()),
7867                    oid: commit.clone(),
7868                    not_for_merge: false,
7869                    force: false,
7870                },
7871                FetchRefUpdate {
7872                    src: "refs/tags/v1".into(),
7873                    dst: Some("refs/tags/v1".into()),
7874                    oid: commit.clone(),
7875                    not_for_merge: true,
7876                    force: false,
7877                },
7878            ]
7879        );
7880        assert_eq!(
7881            fetch_ref_updates_to_fetch_head(&updates, "../bundle.bdl")
7882                .expect("test operation should succeed"),
7883            vec![
7884                FetchHeadRecord {
7885                    oid: commit.clone(),
7886                    not_for_merge: false,
7887                    description: "branch 'main' of ../bundle.bdl".into(),
7888                },
7889                FetchHeadRecord {
7890                    oid: commit,
7891                    not_for_merge: true,
7892                    description: "tag 'v1' of ../bundle.bdl".into(),
7893                },
7894            ]
7895        );
7896    }
7897
7898    #[test]
7899    fn fetch_planner_rejects_missing_or_sourceless_refspecs() {
7900        let refs = vec![RefAdvertisement {
7901            oid: ObjectId::from_hex(
7902                ObjectFormat::Sha1,
7903                "1111111111111111111111111111111111111111",
7904            )
7905            .expect("test operation should succeed"),
7906            name: "refs/heads/main".into(),
7907            capabilities: Vec::new(),
7908        }];
7909        assert!(
7910            plan_fetch_ref_updates(
7911                &refs,
7912                &[parse_refspec("refs/heads/missing").expect("test operation should succeed")],
7913                false
7914            )
7915            .is_err()
7916        );
7917        assert!(
7918            plan_fetch_ref_updates(
7919                &refs,
7920                &[parse_refspec(":refs/heads/main").expect("test operation should succeed")],
7921                false
7922            )
7923            .is_err()
7924        );
7925    }
7926
7927    #[test]
7928    fn fetch_planner_sourceless_positive_refspec_returns_err_not_panic() {
7929        // Regression guard for the sley#7 conversion at the `let Some(src) = ..`
7930        // binding: a non-pattern positive refspec with no source must return an
7931        // error, never panic. Construct the malformed RefSpec directly so the
7932        // test pins the converted guard rather than parse_refspec's behavior.
7933        let refs = vec![RefAdvertisement {
7934            oid: ObjectId::from_hex(
7935                ObjectFormat::Sha1,
7936                "1111111111111111111111111111111111111111",
7937            )
7938            .expect("test operation should succeed"),
7939            name: "refs/heads/main".into(),
7940            capabilities: Vec::new(),
7941        }];
7942        let malformed = RefSpec {
7943            force: false,
7944            negative: false,
7945            src: None,
7946            dst: Some("refs/heads/main".into()),
7947            pattern: false,
7948        };
7949        let result = plan_fetch_ref_updates(&refs, &[malformed], false);
7950        assert!(
7951            result.is_err(),
7952            "sourceless positive refspec must yield Err, got {result:?}"
7953        );
7954    }
7955
7956    #[test]
7957    fn push_planner_builds_create_update_delete_and_matching_commands() {
7958        let old = ObjectId::from_hex(
7959            ObjectFormat::Sha1,
7960            "1111111111111111111111111111111111111111",
7961        )
7962        .expect("test operation should succeed");
7963        let new = ObjectId::from_hex(
7964            ObjectFormat::Sha1,
7965            "2222222222222222222222222222222222222222",
7966        )
7967        .expect("test operation should succeed");
7968        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
7969        let local_refs = vec![
7970            PushSourceRef {
7971                oid: new.clone(),
7972                name: "refs/heads/main".into(),
7973            },
7974            PushSourceRef {
7975                oid: new.clone(),
7976                name: "refs/heads/new".into(),
7977            },
7978        ];
7979        let remote_refs = vec![
7980            RefAdvertisement {
7981                oid: old.clone(),
7982                name: "refs/heads/main".into(),
7983                capabilities: Vec::new(),
7984            },
7985            RefAdvertisement {
7986                oid: old.clone(),
7987                name: "refs/heads/old".into(),
7988                capabilities: Vec::new(),
7989            },
7990        ];
7991
7992        assert_eq!(
7993            plan_push_commands(
7994                ObjectFormat::Sha1,
7995                &local_refs,
7996                &remote_refs,
7997                &[parse_refspec("refs/heads/main").expect("test operation should succeed")],
7998            )
7999            .expect("test operation should succeed"),
8000            vec![ReceivePackCommand {
8001                old_id: old.clone(),
8002                new_id: new.clone(),
8003                name: "refs/heads/main".into(),
8004            }]
8005        );
8006        assert_eq!(
8007            plan_push_commands(
8008                ObjectFormat::Sha1,
8009                &local_refs,
8010                &remote_refs,
8011                &[parse_refspec("refs/heads/new:refs/heads/new")
8012                    .expect("test operation should succeed")],
8013            )
8014            .expect("test operation should succeed"),
8015            vec![ReceivePackCommand {
8016                old_id: zero.clone(),
8017                new_id: new.clone(),
8018                name: "refs/heads/new".into(),
8019            }]
8020        );
8021        assert_eq!(
8022            plan_push_commands(
8023                ObjectFormat::Sha1,
8024                &local_refs,
8025                &remote_refs,
8026                &[parse_refspec(":refs/heads/old").expect("test operation should succeed")],
8027            )
8028            .expect("test operation should succeed"),
8029            vec![ReceivePackCommand {
8030                old_id: old.clone(),
8031                new_id: zero,
8032                name: "refs/heads/old".into(),
8033            }]
8034        );
8035        assert_eq!(
8036            plan_push_commands(
8037                ObjectFormat::Sha1,
8038                &local_refs,
8039                &remote_refs,
8040                &[parse_refspec(":").expect("test operation should succeed")],
8041            )
8042            .expect("test operation should succeed"),
8043            vec![ReceivePackCommand {
8044                old_id: old,
8045                new_id: new,
8046                name: "refs/heads/main".into(),
8047            }]
8048        );
8049    }
8050
8051    #[test]
8052    fn push_planner_builds_wildcard_commands_and_rejects_bad_refspecs() {
8053        let new = ObjectId::from_hex(
8054            ObjectFormat::Sha1,
8055            "2222222222222222222222222222222222222222",
8056        )
8057        .expect("test operation should succeed");
8058        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
8059        let local_refs = vec![PushSourceRef {
8060            oid: new.clone(),
8061            name: "refs/heads/topic".into(),
8062        }];
8063        let commands = plan_push_commands(
8064            ObjectFormat::Sha1,
8065            &local_refs,
8066            &[],
8067            &[parse_refspec("refs/heads/*:refs/heads/review/*")
8068                .expect("test operation should succeed")],
8069        )
8070        .expect("test operation should succeed");
8071        assert_eq!(
8072            commands,
8073            vec![ReceivePackCommand {
8074                old_id: zero,
8075                new_id: new,
8076                name: "refs/heads/review/topic".into(),
8077            }]
8078        );
8079        assert!(
8080            plan_push_commands(
8081                ObjectFormat::Sha1,
8082                &local_refs,
8083                &[],
8084                &[parse_refspec("^refs/heads/topic").expect("test operation should succeed")],
8085            )
8086            .is_err()
8087        );
8088        assert_eq!(
8089            plan_push_commands(
8090                ObjectFormat::Sha1,
8091                &local_refs,
8092                &[],
8093                &[parse_refspec(":refs/heads/missing").expect("test operation should succeed")],
8094            )
8095            .expect("missing deletes are sent as zero-to-zero commands"),
8096            vec![ReceivePackCommand {
8097                old_id: zero,
8098                new_id: zero,
8099                name: "refs/heads/missing".into(),
8100            }]
8101        );
8102    }
8103
8104    #[test]
8105    fn receive_pack_push_request_builder_negotiates_capabilities() {
8106        let old_id = ObjectId::from_hex(
8107            ObjectFormat::Sha1,
8108            "1111111111111111111111111111111111111111",
8109        )
8110        .expect("test operation should succeed");
8111        let new_id = ObjectId::from_hex(
8112            ObjectFormat::Sha1,
8113            "2222222222222222222222222222222222222222",
8114        )
8115        .expect("test operation should succeed");
8116        let features = ReceivePackFeatures {
8117            report_status_v2: true,
8118            atomic: true,
8119            ofs_delta: true,
8120            push_options: true,
8121            side_band_64k: true,
8122            quiet: true,
8123            object_format: Some(ObjectFormat::Sha1),
8124            ..ReceivePackFeatures::default()
8125        };
8126        let request = build_receive_pack_push_request(
8127            &features,
8128            vec![ReceivePackCommand {
8129                old_id,
8130                new_id,
8131                name: "refs/heads/main".into(),
8132            }],
8133            b"PACKdata".to_vec(),
8134            ReceivePackPushRequestOptions {
8135                report_status_v2: true,
8136                atomic: true,
8137                ofs_delta: true,
8138                side_band_64k: true,
8139                quiet: true,
8140                agent: Some("sley/0".into()),
8141                object_format: Some(ObjectFormat::Sha1),
8142                push_options: vec!["ci.skip".into()],
8143                ..ReceivePackPushRequestOptions::default()
8144            },
8145        )
8146        .expect("test operation should succeed");
8147        assert_eq!(
8148            request.commands.capabilities,
8149            vec![
8150                Capability {
8151                    name: "report-status-v2".into(),
8152                    value: None,
8153                },
8154                Capability {
8155                    name: "atomic".into(),
8156                    value: None,
8157                },
8158                Capability {
8159                    name: "ofs-delta".into(),
8160                    value: None,
8161                },
8162                Capability {
8163                    name: "side-band-64k".into(),
8164                    value: None,
8165                },
8166                Capability {
8167                    name: "quiet".into(),
8168                    value: None,
8169                },
8170                Capability {
8171                    name: "agent".into(),
8172                    value: Some("sley/0".into()),
8173                },
8174                Capability {
8175                    name: "object-format".into(),
8176                    value: Some("sha1".into()),
8177                },
8178                Capability {
8179                    name: "push-options".into(),
8180                    value: None,
8181                },
8182            ]
8183        );
8184        assert_eq!(request.push_options, Some(vec!["ci.skip".into()]));
8185        validate_receive_pack_push_request_features(&features, &request)
8186            .expect("test operation should succeed");
8187    }
8188
8189    #[test]
8190    fn receive_pack_push_request_builder_handles_delete_only_and_rejects_unadvertised() {
8191        let old_id = ObjectId::from_hex(
8192            ObjectFormat::Sha1,
8193            "1111111111111111111111111111111111111111",
8194        )
8195        .expect("test operation should succeed");
8196        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
8197        let features = ReceivePackFeatures {
8198            delete_refs: true,
8199            ..ReceivePackFeatures::default()
8200        };
8201        let request = build_receive_pack_push_request(
8202            &features,
8203            vec![ReceivePackCommand {
8204                old_id,
8205                new_id: zero,
8206                name: "refs/heads/old".into(),
8207            }],
8208            Vec::new(),
8209            ReceivePackPushRequestOptions::default(),
8210        )
8211        .expect("test operation should succeed");
8212        assert_eq!(
8213            request.commands.capabilities,
8214            vec![Capability {
8215                name: "delete-refs".into(),
8216                value: None,
8217            }]
8218        );
8219        assert!(request.packfile.is_empty());
8220
8221        assert!(
8222            build_receive_pack_push_request(
8223                &ReceivePackFeatures::default(),
8224                request.commands.commands.clone(),
8225                Vec::new(),
8226                ReceivePackPushRequestOptions::default(),
8227            )
8228            .is_err()
8229        );
8230        assert!(
8231            build_receive_pack_push_request(
8232                &features,
8233                request.commands.commands,
8234                b"PACK".to_vec(),
8235                ReceivePackPushRequestOptions::default(),
8236            )
8237            .is_err()
8238        );
8239        assert!(
8240            build_receive_pack_push_request(
8241                &features,
8242                Vec::new(),
8243                Vec::new(),
8244                ReceivePackPushRequestOptions {
8245                    push_options: vec!["ci.skip".into()],
8246                    ..ReceivePackPushRequestOptions::default()
8247                },
8248            )
8249            .is_err()
8250        );
8251    }
8252
8253    #[test]
8254    fn smart_http_helpers_build_paths_and_content_types() {
8255        let sha1 = ObjectId::from_hex(
8256            ObjectFormat::Sha1,
8257            "1111111111111111111111111111111111111111",
8258        )
8259        .expect("test operation should succeed");
8260        let sha256 = ObjectId::from_hex(
8261            ObjectFormat::Sha256,
8262            "2222222222222222222222222222222222222222222222222222222222222222",
8263        )
8264        .expect("test operation should succeed");
8265        assert_eq!(
8266            smart_http_info_refs_path("/repo.git/", GitService::UploadPack)
8267                .expect("test operation should succeed"),
8268            "/repo.git/info/refs?service=git-upload-pack"
8269        );
8270        assert_eq!(
8271            dumb_http_info_refs_path("/repo.git/").expect("test operation should succeed"),
8272            "/repo.git/info/refs"
8273        );
8274        assert_eq!(
8275            dumb_http_alternates_path("/repo.git").expect("test operation should succeed"),
8276            "/repo.git/objects/info/http-alternates"
8277        );
8278        assert_eq!(
8279            dumb_http_packs_path("/repo.git/").expect("test operation should succeed"),
8280            "/repo.git/objects/info/packs"
8281        );
8282        assert_eq!(
8283            dumb_http_loose_object_path("/repo.git/", &sha1)
8284                .expect("test operation should succeed"),
8285            "/repo.git/objects/11/11111111111111111111111111111111111111"
8286        );
8287        assert_eq!(
8288            dumb_http_loose_object_path("/repo.git/", &sha256)
8289                .expect("test operation should succeed"),
8290            "/repo.git/objects/22/22222222222222222222222222222222222222222222222222222222222222"
8291        );
8292        assert_eq!(
8293            dumb_http_pack_file_path("/repo.git/", &sha1).expect("test operation should succeed"),
8294            "/repo.git/objects/pack/pack-1111111111111111111111111111111111111111.pack"
8295        );
8296        assert_eq!(
8297            dumb_http_pack_index_path("/repo.git/", &sha1).expect("test operation should succeed"),
8298            "/repo.git/objects/pack/pack-1111111111111111111111111111111111111111.idx"
8299        );
8300        assert_eq!(
8301            smart_http_rpc_path("/repo.git", GitService::ReceivePack)
8302                .expect("test operation should succeed"),
8303            "/repo.git/git-receive-pack"
8304        );
8305        assert_eq!(
8306            smart_http_advertisement_content_type(GitService::UploadPack)
8307                .expect("test operation should succeed"),
8308            "application/x-git-upload-pack-advertisement"
8309        );
8310        assert_eq!(
8311            smart_http_rpc_request_content_type(GitService::UploadPack)
8312                .expect("test operation should succeed"),
8313            "application/x-git-upload-pack-request"
8314        );
8315        assert_eq!(
8316            smart_http_rpc_result_content_type(GitService::ReceivePack)
8317                .expect("test operation should succeed"),
8318            "application/x-git-receive-pack-result"
8319        );
8320        assert_eq!(
8321            parse_smart_http_advertisement_content_type(
8322                "Application/X-Git-Upload-Pack-Advertisement"
8323            )
8324            .expect("test operation should succeed"),
8325            GitService::UploadPack
8326        );
8327        assert_eq!(
8328            parse_smart_http_rpc_request_content_type("application/x-git-receive-pack-request")
8329                .expect("test operation should succeed"),
8330            GitService::ReceivePack
8331        );
8332        assert_eq!(
8333            parse_smart_http_rpc_result_content_type("application/x-git-upload-pack-result")
8334                .expect("test operation should succeed"),
8335            GitService::UploadPack
8336        );
8337    }
8338
8339    #[test]
8340    fn smart_http_helpers_reject_invalid_services_paths_and_content_types() {
8341        let oid = ObjectId::from_hex(
8342            ObjectFormat::Sha1,
8343            "1111111111111111111111111111111111111111",
8344        )
8345        .expect("test operation should succeed");
8346        assert!(smart_http_info_refs_path("repo.git", GitService::UploadPack).is_err());
8347        assert!(smart_http_rpc_path("/repo.git?x=1", GitService::UploadPack).is_err());
8348        assert!(dumb_http_info_refs_path("repo.git").is_err());
8349        assert!(dumb_http_alternates_path("/repo.git#fragment").is_err());
8350        assert!(dumb_http_packs_path("/repo.git?query").is_err());
8351        assert!(dumb_http_loose_object_path("repo.git", &oid).is_err());
8352        assert!(dumb_http_pack_file_path("/repo.git#fragment", &oid).is_err());
8353        assert!(dumb_http_pack_index_path("/repo.git?query", &oid).is_err());
8354        assert!(smart_http_info_refs_path("/repo.git", GitService::UploadArchive).is_err());
8355        assert!(smart_http_advertisement_content_type(GitService::UploadArchive).is_err());
8356        assert!(
8357            parse_smart_http_advertisement_content_type(
8358                "application/x-git-upload-archive-advertisement"
8359            )
8360            .is_err()
8361        );
8362        assert!(
8363            parse_smart_http_rpc_request_content_type("application/x-git-upload-pack-result")
8364                .is_err()
8365        );
8366        assert!(
8367            parse_smart_http_rpc_result_content_type(
8368                "application/x-git-receive-pack-result; charset=utf-8"
8369            )
8370            .is_err()
8371        );
8372    }
8373
8374    #[test]
8375    fn sideband_packets_parse_and_encode_channels() {
8376        let payloads = vec![
8377            b"\x01PACK bytes".to_vec(),
8378            b"\x02counting objects\n".to_vec(),
8379            b"\x03fatal error\n".to_vec(),
8380        ];
8381        let packets = parse_sideband_packets(&payloads).expect("test operation should succeed");
8382        assert_eq!(
8383            packets,
8384            vec![
8385                SideBandPacket {
8386                    channel: SideBandChannel::Data,
8387                    data: b"PACK bytes".to_vec(),
8388                },
8389                SideBandPacket {
8390                    channel: SideBandChannel::Progress,
8391                    data: b"counting objects\n".to_vec(),
8392                },
8393                SideBandPacket {
8394                    channel: SideBandChannel::Fatal,
8395                    data: b"fatal error\n".to_vec(),
8396                },
8397            ]
8398        );
8399        assert_eq!(
8400            encode_sideband_packets(&packets).expect("test operation should succeed"),
8401            payloads
8402        );
8403    }
8404
8405    #[test]
8406    fn sideband_stream_parses_encodes_and_demuxes_packets() {
8407        let frames = vec![
8408            PktLineFrame::Data(vec![1, b'P', b'A']),
8409            PktLineFrame::Data(vec![2, b'c', b'o', b'u', b'n', b't', b'\n']),
8410            PktLineFrame::Data(vec![1, b'C', b'K']),
8411            PktLineFrame::Flush,
8412        ];
8413        let packets = parse_sideband_stream(&frames).expect("test operation should succeed");
8414        assert_eq!(
8415            packets,
8416            vec![
8417                SideBandPacket {
8418                    channel: SideBandChannel::Data,
8419                    data: b"PA".to_vec(),
8420                },
8421                SideBandPacket {
8422                    channel: SideBandChannel::Progress,
8423                    data: b"count\n".to_vec(),
8424                },
8425                SideBandPacket {
8426                    channel: SideBandChannel::Data,
8427                    data: b"CK".to_vec(),
8428                },
8429            ]
8430        );
8431        assert_eq!(
8432            encode_sideband_stream(&packets).expect("test operation should succeed"),
8433            frames
8434        );
8435        assert_eq!(
8436            demux_sideband_stream(&frames).expect("test operation should succeed"),
8437            SideBandDemux {
8438                data: b"PACK".to_vec(),
8439                progress: vec![b"count\n".to_vec()],
8440            }
8441        );
8442    }
8443
8444    #[test]
8445    fn sideband_stream_reads_and_writes_until_flush() {
8446        let packets = vec![
8447            SideBandPacket {
8448                channel: SideBandChannel::Data,
8449                data: b"PACK".to_vec(),
8450            },
8451            SideBandPacket {
8452                channel: SideBandChannel::Progress,
8453                data: b"done\n".to_vec(),
8454            },
8455        ];
8456        let mut encoded = Vec::new();
8457        write_sideband_stream(&mut encoded, &packets).expect("test operation should succeed");
8458        encoded.extend_from_slice(b"tail");
8459
8460        let mut input = encoded.as_slice();
8461        assert_eq!(
8462            read_sideband_stream(&mut input).expect("test operation should succeed"),
8463            packets
8464        );
8465        assert_eq!(input, b"tail");
8466
8467        let mut input = encoded.as_slice();
8468        assert_eq!(
8469            read_and_demux_sideband_stream(&mut input).expect("test operation should succeed"),
8470            SideBandDemux {
8471                data: b"PACK".to_vec(),
8472                progress: vec![b"done\n".to_vec()],
8473            }
8474        );
8475        assert_eq!(input, b"tail");
8476    }
8477
8478    #[test]
8479    fn sideband_packets_demux_data_and_progress() {
8480        let payloads = vec![
8481            b"\x01PACK".to_vec(),
8482            b"\x02counting objects\n".to_vec(),
8483            b"\x01 bytes".to_vec(),
8484            b"\x02done\n".to_vec(),
8485        ];
8486        assert_eq!(
8487            parse_and_demux_sideband_packets(&payloads).expect("test operation should succeed"),
8488            SideBandDemux {
8489                data: b"PACK bytes".to_vec(),
8490                progress: vec![b"counting objects\n".to_vec(), b"done\n".to_vec()],
8491            }
8492        );
8493    }
8494
8495    #[test]
8496    fn sideband_packets_reject_bad_channels_and_oversize_payloads() {
8497        assert!(parse_sideband_packet(b"").is_err());
8498        assert!(parse_sideband_packet(b"\x04bad").is_err());
8499        assert!(
8500            parse_sideband_stream(&[PktLineFrame::Data(vec![1, b'P', b'A', b'C', b'K'])]).is_err()
8501        );
8502        assert!(parse_sideband_stream(&[PktLineFrame::Delimiter, PktLineFrame::Flush]).is_err());
8503        assert!(
8504            parse_sideband_stream(&[
8505                PktLineFrame::Data(vec![1, b'P', b'A']),
8506                PktLineFrame::Flush,
8507                PktLineFrame::Data(vec![1, b'C', b'K']),
8508            ])
8509            .is_err()
8510        );
8511        assert!(
8512            parse_sideband_stream(&[
8513                PktLineFrame::Data(vec![1, b'P', b'A']),
8514                PktLineFrame::Data(b"\x04bad".to_vec()),
8515                PktLineFrame::Flush,
8516            ])
8517            .is_err()
8518        );
8519        assert!(
8520            encode_sideband_packet(&SideBandPacket {
8521                channel: SideBandChannel::Data,
8522                data: vec![0; PKT_LINE_MAX_PAYLOAD_LEN],
8523            })
8524            .is_err()
8525        );
8526        assert!(
8527            demux_sideband_packets(&[SideBandPacket {
8528                channel: SideBandChannel::Fatal,
8529                data: b"remote died\n".to_vec(),
8530            }])
8531            .is_err()
8532        );
8533    }
8534
8535    #[test]
8536    fn upload_archive_request_parses_and_encodes_arguments() {
8537        let frames = vec![
8538            PktLineFrame::Data(b"argument --format=tar\n".to_vec()),
8539            PktLineFrame::Data(b"argument HEAD:dir with spaces\n".to_vec()),
8540            PktLineFrame::Flush,
8541        ];
8542        let request = parse_upload_archive_request(&frames).expect("test operation should succeed");
8543        assert_eq!(
8544            request,
8545            UploadArchiveRequest {
8546                arguments: vec!["--format=tar".into(), "HEAD:dir with spaces".into()],
8547            }
8548        );
8549        assert_eq!(
8550            encode_upload_archive_request(&request).expect("test operation should succeed"),
8551            frames
8552        );
8553    }
8554
8555    #[test]
8556    fn upload_archive_request_streams_round_trip() {
8557        let request = UploadArchiveRequest {
8558            arguments: vec!["--prefix=src/".into(), "main".into()],
8559        };
8560        let mut encoded = Vec::new();
8561        write_upload_archive_request(&mut encoded, &request)
8562            .expect("test operation should succeed");
8563        encoded.extend_from_slice(b"tail");
8564
8565        let mut input = encoded.as_slice();
8566        assert_eq!(
8567            read_upload_archive_request(&mut input).expect("test operation should succeed"),
8568            request
8569        );
8570        assert_eq!(input, b"tail");
8571    }
8572
8573    #[test]
8574    fn upload_archive_request_rejects_malformed_streams() {
8575        assert!(parse_upload_archive_request(&[PktLineFrame::Flush]).is_err());
8576        assert!(
8577            parse_upload_archive_request(&[
8578                PktLineFrame::Data(b"--format=tar\n".to_vec()),
8579                PktLineFrame::Flush,
8580            ])
8581            .is_err()
8582        );
8583        assert!(
8584            parse_upload_archive_request(&[
8585                PktLineFrame::Data(b"argument HEAD\n".to_vec()),
8586                PktLineFrame::Delimiter,
8587                PktLineFrame::Flush,
8588            ])
8589            .is_err()
8590        );
8591        assert!(
8592            encode_upload_archive_request(&UploadArchiveRequest {
8593                arguments: vec!["bad\narg".into()],
8594            })
8595            .is_err()
8596        );
8597    }
8598
8599    #[test]
8600    fn upload_archive_response_parses_ack_sideband_and_nack() {
8601        let ack_frames = vec![
8602            PktLineFrame::Data(b"ACK\n".to_vec()),
8603            PktLineFrame::Data(b"\x01tar bytes".to_vec()),
8604            PktLineFrame::Data(b"\x02progress\n".to_vec()),
8605            PktLineFrame::Flush,
8606        ];
8607        let response =
8608            parse_upload_archive_response(&ack_frames).expect("test operation should succeed");
8609        assert_eq!(
8610            response,
8611            UploadArchiveResponse::Ack {
8612                sideband: vec![
8613                    SideBandPacket {
8614                        channel: SideBandChannel::Data,
8615                        data: b"tar bytes".to_vec(),
8616                    },
8617                    SideBandPacket {
8618                        channel: SideBandChannel::Progress,
8619                        data: b"progress\n".to_vec(),
8620                    },
8621                ],
8622            }
8623        );
8624        assert_eq!(
8625            encode_upload_archive_response(&response).expect("test operation should succeed"),
8626            ack_frames
8627        );
8628        assert_eq!(
8629            demux_upload_archive_response(&response).expect("test operation should succeed"),
8630            SideBandDemux {
8631                data: b"tar bytes".to_vec(),
8632                progress: vec![b"progress\n".to_vec()],
8633            }
8634        );
8635
8636        let nack = UploadArchiveResponse::Nack {
8637            message: "unreachable tree".into(),
8638        };
8639        let nack_frames = vec![
8640            PktLineFrame::Data(b"NACK unreachable tree\n".to_vec()),
8641            PktLineFrame::Flush,
8642        ];
8643        assert_eq!(
8644            parse_upload_archive_response(&nack_frames).expect("test operation should succeed"),
8645            nack
8646        );
8647        assert_eq!(
8648            encode_upload_archive_response(&nack).expect("test operation should succeed"),
8649            nack_frames
8650        );
8651        assert!(demux_upload_archive_response(&nack).is_err());
8652    }
8653
8654    #[test]
8655    fn upload_archive_response_streams_round_trip() {
8656        let response = UploadArchiveResponse::Ack {
8657            sideband: vec![SideBandPacket {
8658                channel: SideBandChannel::Data,
8659                data: b"tar bytes".to_vec(),
8660            }],
8661        };
8662        let mut encoded = Vec::new();
8663        write_upload_archive_response(&mut encoded, &response)
8664            .expect("test operation should succeed");
8665        encoded.extend_from_slice(b"tail");
8666
8667        let mut input = encoded.as_slice();
8668        assert_eq!(
8669            read_upload_archive_response(&mut input).expect("test operation should succeed"),
8670            response
8671        );
8672        assert_eq!(input, b"tail");
8673    }
8674
8675    #[test]
8676    fn upload_archive_response_rejects_malformed_streams() {
8677        assert!(parse_upload_archive_response(&[]).is_err());
8678        assert!(
8679            parse_upload_archive_response(&[
8680                PktLineFrame::Data(b"ACK\n".to_vec()),
8681                PktLineFrame::Flush,
8682                PktLineFrame::Data(b"\x01tail".to_vec()),
8683            ])
8684            .is_err()
8685        );
8686        assert!(
8687            parse_upload_archive_response(&[
8688                PktLineFrame::Data(b"NACK\n".to_vec()),
8689                PktLineFrame::Flush,
8690            ])
8691            .is_err()
8692        );
8693        assert!(
8694            parse_upload_archive_response(&[
8695                PktLineFrame::Data(b"NACK denied\n".to_vec()),
8696                PktLineFrame::Data(b"\x02extra\n".to_vec()),
8697                PktLineFrame::Flush,
8698            ])
8699            .is_err()
8700        );
8701        assert!(
8702            encode_upload_archive_response(&UploadArchiveResponse::Nack {
8703                message: "bad\nmessage".into(),
8704            })
8705            .is_err()
8706        );
8707    }
8708
8709    #[test]
8710    fn capabilities_parse_and_encode_tokens() {
8711        let capabilities = parse_capabilities(
8712            b"multi_ack thin-pack agent=git/2.54.0 symref=HEAD:refs/heads/main\n",
8713        )
8714        .expect("test operation should succeed");
8715        assert_eq!(
8716            capabilities,
8717            vec![
8718                Capability {
8719                    name: "multi_ack".into(),
8720                    value: None,
8721                },
8722                Capability {
8723                    name: "thin-pack".into(),
8724                    value: None,
8725                },
8726                Capability {
8727                    name: "agent".into(),
8728                    value: Some("git/2.54.0".into()),
8729                },
8730                Capability {
8731                    name: "symref".into(),
8732                    value: Some("HEAD:refs/heads/main".into()),
8733                },
8734            ]
8735        );
8736        assert_eq!(
8737            encode_capabilities(&capabilities).expect("test operation should succeed"),
8738            b"multi_ack thin-pack agent=git/2.54.0 symref=HEAD:refs/heads/main"
8739        );
8740    }
8741
8742    #[test]
8743    fn capabilities_reject_empty_or_delimited_fields() {
8744        assert!(parse_capabilities(b"multi_ack  thin-pack").is_err());
8745        assert!(parse_capabilities(b"agent=").is_err());
8746        assert!(parse_capabilities(b"symref=HEAD:refs/heads/main\nbad").is_err());
8747        assert!(
8748            encode_capabilities(&[Capability {
8749                name: "bad name".into(),
8750                value: None,
8751            }])
8752            .is_err()
8753        );
8754    }
8755
8756    #[test]
8757    fn protocol_v2_object_format_uses_capability_or_defaults_to_sha1() {
8758        assert_eq!(
8759            protocol_v2_object_format(&[]).expect("test operation should succeed"),
8760            ObjectFormat::Sha1
8761        );
8762        assert_eq!(
8763            protocol_v2_object_format(&[Capability {
8764                name: "object-format".into(),
8765                value: Some("sha256".into()),
8766            }])
8767            .expect("test operation should succeed"),
8768            ObjectFormat::Sha256
8769        );
8770        assert!(
8771            protocol_v2_object_format(&[Capability {
8772                name: "object-format".into(),
8773                value: None,
8774            }])
8775            .is_err()
8776        );
8777        assert!(
8778            protocol_v2_object_format(&[
8779                Capability {
8780                    name: "object-format".into(),
8781                    value: Some("sha1".into()),
8782                },
8783                Capability {
8784                    name: "object-format".into(),
8785                    value: Some("sha256".into()),
8786                },
8787            ])
8788            .is_err()
8789        );
8790        assert!(
8791            protocol_v2_object_format(&[Capability {
8792                name: "object-format".into(),
8793                value: Some("unknown".into()),
8794            }])
8795            .is_err()
8796        );
8797    }
8798
8799    #[test]
8800    fn protocol_v2_command_request_capabilities_validate_against_handshake() {
8801        let handshake = TransportHandshake {
8802            protocol: ProtocolVersion::V2,
8803            capabilities: vec![
8804                Capability {
8805                    name: "fetch".into(),
8806                    value: Some("shallow filter".into()),
8807                },
8808                Capability {
8809                    name: "agent".into(),
8810                    value: Some("sley/0".into()),
8811                },
8812                Capability {
8813                    name: "object-format".into(),
8814                    value: Some("sha1".into()),
8815                },
8816            ],
8817        };
8818        validate_protocol_v2_command_request_capabilities(
8819            &handshake,
8820            &ProtocolV2CommandRequest {
8821                command: "fetch".into(),
8822                capabilities: vec![
8823                    Capability {
8824                        name: "agent".into(),
8825                        value: Some("client/1".into()),
8826                    },
8827                    Capability {
8828                        name: "object-format".into(),
8829                        value: Some("sha1".into()),
8830                    },
8831                ],
8832                arguments: Vec::new(),
8833            },
8834        )
8835        .expect("test operation should succeed");
8836        assert!(
8837            validate_protocol_v2_command_request_capabilities(
8838                &handshake,
8839                &ProtocolV2CommandRequest {
8840                    command: "ls-refs".into(),
8841                    capabilities: Vec::new(),
8842                    arguments: Vec::new(),
8843                },
8844            )
8845            .is_err()
8846        );
8847        assert!(
8848            validate_protocol_v2_command_request_capabilities(
8849                &handshake,
8850                &ProtocolV2CommandRequest {
8851                    command: "fetch".into(),
8852                    capabilities: vec![Capability {
8853                        name: "server-option".into(),
8854                        value: None,
8855                    }],
8856                    arguments: Vec::new(),
8857                },
8858            )
8859            .is_err()
8860        );
8861        assert!(
8862            validate_protocol_v2_command_request_capabilities(
8863                &handshake,
8864                &ProtocolV2CommandRequest {
8865                    command: "fetch".into(),
8866                    capabilities: vec![Capability {
8867                        name: "object-format".into(),
8868                        value: Some("sha256".into()),
8869                    }],
8870                    arguments: Vec::new(),
8871                },
8872            )
8873            .is_err()
8874        );
8875        assert!(
8876            validate_protocol_v2_command_request_capabilities(
8877                &handshake,
8878                &ProtocolV2CommandRequest {
8879                    command: "fetch".into(),
8880                    capabilities: vec![Capability {
8881                        name: "agent".into(),
8882                        value: None,
8883                    }],
8884                    arguments: Vec::new(),
8885                },
8886            )
8887            .is_err()
8888        );
8889    }
8890
8891    #[test]
8892    fn protocol_v2_command_options_parse_and_encode_known_capabilities() {
8893        let capabilities = vec![
8894            Capability {
8895                name: "agent".into(),
8896                value: Some("sley/0".into()),
8897            },
8898            Capability {
8899                name: "object-format".into(),
8900                value: Some("sha256".into()),
8901            },
8902            Capability {
8903                name: "server-option".into(),
8904                value: Some("trace=true".into()),
8905            },
8906            Capability {
8907                name: "server-option".into(),
8908                value: Some("region=west".into()),
8909            },
8910            Capability {
8911                name: "session-id".into(),
8912                value: Some("abc123".into()),
8913            },
8914        ];
8915        let options = parse_protocol_v2_command_options(&capabilities)
8916            .expect("test operation should succeed");
8917        assert_eq!(
8918            options,
8919            ProtocolV2CommandOptions {
8920                agent: Some("sley/0".into()),
8921                object_format: Some(ObjectFormat::Sha256),
8922                server_options: vec!["trace=true".into(), "region=west".into()],
8923                extra: vec![Capability {
8924                    name: "session-id".into(),
8925                    value: Some("abc123".into()),
8926                }],
8927            }
8928        );
8929        assert_eq!(
8930            encode_protocol_v2_command_options(&options).expect("test operation should succeed"),
8931            capabilities
8932        );
8933    }
8934
8935    #[test]
8936    fn protocol_v2_command_options_reject_malformed_known_capabilities() {
8937        assert!(
8938            parse_protocol_v2_command_options(&[
8939                Capability {
8940                    name: "agent".into(),
8941                    value: Some("sley/0".into()),
8942                },
8943                Capability {
8944                    name: "agent".into(),
8945                    value: Some("sley/1".into()),
8946                },
8947            ])
8948            .is_err()
8949        );
8950        assert!(
8951            parse_protocol_v2_command_options(&[Capability {
8952                name: "object-format".into(),
8953                value: Some("sha512".into()),
8954            }])
8955            .is_err()
8956        );
8957        assert!(
8958            parse_protocol_v2_command_options(&[Capability {
8959                name: "server-option".into(),
8960                value: None,
8961            }])
8962            .is_err()
8963        );
8964        assert!(
8965            encode_protocol_v2_command_options(&ProtocolV2CommandOptions {
8966                extra: vec![Capability {
8967                    name: "server-option".into(),
8968                    value: Some("trace=true".into()),
8969                }],
8970                ..ProtocolV2CommandOptions::default()
8971            })
8972            .is_err()
8973        );
8974    }
8975
8976    #[test]
8977    fn protocol_v2_ls_refs_features_parse_and_encode_advertisement() {
8978        let capabilities = vec![Capability {
8979            name: "ls-refs".into(),
8980            value: Some("unborn custom".into()),
8981        }];
8982        let features = parse_protocol_v2_ls_refs_features(&capabilities)
8983            .expect("test operation should succeed")
8984            .expect("test operation should succeed");
8985        assert_eq!(
8986            features,
8987            ProtocolV2LsRefsFeatures {
8988                unborn: true,
8989                unknown: vec!["custom".into()],
8990            }
8991        );
8992        assert_eq!(
8993            encode_protocol_v2_ls_refs_capability(&features)
8994                .expect("test operation should succeed"),
8995            capabilities[0]
8996        );
8997        assert_eq!(
8998            parse_protocol_v2_ls_refs_features(&[Capability {
8999                name: "ls-refs".into(),
9000                value: None,
9001            }])
9002            .expect("test operation should succeed")
9003            .expect("test operation should succeed"),
9004            ProtocolV2LsRefsFeatures::default()
9005        );
9006        assert!(
9007            parse_protocol_v2_ls_refs_features(&[Capability {
9008                name: "fetch".into(),
9009                value: Some("filter".into()),
9010            }])
9011            .expect("test operation should succeed")
9012            .is_none()
9013        );
9014    }
9015
9016    #[test]
9017    fn protocol_v2_ls_refs_features_reject_malformed_advertisements() {
9018        assert!(
9019            parse_protocol_v2_ls_refs_features(&[
9020                Capability {
9021                    name: "ls-refs".into(),
9022                    value: None,
9023                },
9024                Capability {
9025                    name: "ls-refs".into(),
9026                    value: None,
9027                },
9028            ])
9029            .is_err()
9030        );
9031        assert!(
9032            parse_protocol_v2_ls_refs_features(&[Capability {
9033                name: "ls-refs".into(),
9034                value: Some("unborn  custom".into()),
9035            }])
9036            .is_err()
9037        );
9038        assert!(
9039            encode_protocol_v2_ls_refs_capability(&ProtocolV2LsRefsFeatures {
9040                unknown: vec!["unborn".into()],
9041                ..ProtocolV2LsRefsFeatures::default()
9042            })
9043            .is_err()
9044        );
9045    }
9046
9047    #[test]
9048    fn protocol_v2_ls_refs_command_request_validates_unborn_feature() {
9049        let handshake = TransportHandshake {
9050            protocol: ProtocolVersion::V2,
9051            capabilities: vec![Capability {
9052                name: "ls-refs".into(),
9053                value: Some("unborn".into()),
9054            }],
9055        };
9056        let request = ProtocolV2CommandRequest {
9057            command: "ls-refs".into(),
9058            capabilities: Vec::new(),
9059            arguments: vec![b"unborn".to_vec(), b"ref-prefix HEAD".to_vec()],
9060        };
9061        let parsed = validate_protocol_v2_ls_refs_command_request(&handshake, &request)
9062            .expect("test operation should succeed");
9063        assert!(parsed.unborn);
9064        assert_eq!(parsed.ref_prefixes, vec!["HEAD"]);
9065
9066        let blocked = TransportHandshake {
9067            protocol: ProtocolVersion::V2,
9068            capabilities: vec![Capability {
9069                name: "ls-refs".into(),
9070                value: None,
9071            }],
9072        };
9073        assert!(validate_protocol_v2_ls_refs_command_request(&blocked, &request).is_err());
9074    }
9075
9076    #[test]
9077    fn protocol_v2_fetch_features_parse_and_encode_advertisement() {
9078        let capabilities = vec![Capability {
9079            name: "fetch".into(),
9080            value: Some(
9081                "shallow wait-for-done filter ref-in-want sideband-all packfile-uris custom".into(),
9082            ),
9083        }];
9084        let features = parse_protocol_v2_fetch_features(&capabilities)
9085            .expect("test operation should succeed")
9086            .expect("test operation should succeed");
9087        assert_eq!(
9088            features,
9089            ProtocolV2FetchFeatures {
9090                shallow: true,
9091                wait_for_done: true,
9092                filter: true,
9093                ref_in_want: true,
9094                sideband_all: true,
9095                packfile_uris: true,
9096                unknown: vec!["custom".into()],
9097            }
9098        );
9099        assert_eq!(
9100            encode_protocol_v2_fetch_capability(&features).expect("test operation should succeed"),
9101            capabilities[0]
9102        );
9103        assert_eq!(
9104            parse_protocol_v2_fetch_features(&[Capability {
9105                name: "fetch".into(),
9106                value: None,
9107            }])
9108            .expect("test operation should succeed")
9109            .expect("test operation should succeed"),
9110            ProtocolV2FetchFeatures::default()
9111        );
9112        assert!(
9113            parse_protocol_v2_fetch_features(&[])
9114                .expect("test operation should succeed")
9115                .is_none()
9116        );
9117    }
9118
9119    #[test]
9120    fn protocol_v2_fetch_features_reject_malformed_advertisements() {
9121        assert!(
9122            parse_protocol_v2_fetch_features(&[
9123                Capability {
9124                    name: "fetch".into(),
9125                    value: None,
9126                },
9127                Capability {
9128                    name: "fetch".into(),
9129                    value: None,
9130                },
9131            ])
9132            .is_err()
9133        );
9134        assert!(
9135            parse_protocol_v2_fetch_features(&[Capability {
9136                name: "fetch".into(),
9137                value: Some("filter  shallow".into()),
9138            }])
9139            .is_err()
9140        );
9141        assert!(
9142            encode_protocol_v2_fetch_capability(&ProtocolV2FetchFeatures {
9143                unknown: vec!["filter".into()],
9144                ..ProtocolV2FetchFeatures::default()
9145            })
9146            .is_err()
9147        );
9148    }
9149
9150    #[test]
9151    fn protocol_v2_fetch_request_features_validate_feature_gated_arguments() {
9152        let features = ProtocolV2FetchFeatures {
9153            shallow: true,
9154            wait_for_done: true,
9155            filter: true,
9156            ref_in_want: true,
9157            sideband_all: true,
9158            packfile_uris: true,
9159            unknown: Vec::new(),
9160        };
9161        validate_protocol_v2_fetch_request_features(
9162            &features,
9163            &ProtocolV2FetchRequest {
9164                want_refs: vec!["refs/heads/main".into()],
9165                shallow: vec![
9166                    ObjectId::from_hex(
9167                        ObjectFormat::Sha1,
9168                        "1111111111111111111111111111111111111111",
9169                    )
9170                    .expect("test operation should succeed"),
9171                ],
9172                deepen: Some(1),
9173                filter: Some("blob:none".into()),
9174                packfile_uris: Some("https".into()),
9175                sideband_all: true,
9176                wait_for_done: true,
9177                ..ProtocolV2FetchRequest::default()
9178            },
9179        )
9180        .expect("test operation should succeed");
9181
9182        let request = ProtocolV2FetchRequest {
9183            want_refs: vec!["refs/heads/main".into()],
9184            filter: Some("blob:none".into()),
9185            sideband_all: true,
9186            ..ProtocolV2FetchRequest::default()
9187        };
9188        assert!(
9189            validate_protocol_v2_fetch_request_features(
9190                &ProtocolV2FetchFeatures::default(),
9191                &request,
9192            )
9193            .is_err()
9194        );
9195        assert!(
9196            validate_protocol_v2_fetch_request_features(
9197                &ProtocolV2FetchFeatures {
9198                    ref_in_want: true,
9199                    filter: true,
9200                    ..ProtocolV2FetchFeatures::default()
9201                },
9202                &request,
9203            )
9204            .is_err()
9205        );
9206    }
9207
9208    #[test]
9209    fn protocol_v2_fetch_command_request_validates_against_handshake_features() {
9210        let handshake = TransportHandshake {
9211            protocol: ProtocolVersion::V2,
9212            capabilities: vec![
9213                Capability {
9214                    name: "fetch".into(),
9215                    value: Some("filter ref-in-want".into()),
9216                },
9217                Capability {
9218                    name: "agent".into(),
9219                    value: Some("sley/0".into()),
9220                },
9221            ],
9222        };
9223        let request = ProtocolV2CommandRequest {
9224            command: "fetch".into(),
9225            capabilities: vec![Capability {
9226                name: "agent".into(),
9227                value: Some("client/1".into()),
9228            }],
9229            arguments: vec![
9230                b"want-ref refs/heads/main".to_vec(),
9231                b"filter blob:none".to_vec(),
9232            ],
9233        };
9234        let fetch =
9235            validate_protocol_v2_fetch_command_request(&handshake, ObjectFormat::Sha1, &request)
9236                .expect("test operation should succeed");
9237        assert_eq!(fetch.want_refs, vec!["refs/heads/main"]);
9238        assert_eq!(fetch.filter.as_deref(), Some("blob:none"));
9239
9240        let mut bad = request.clone();
9241        bad.arguments.push(b"sideband-all".to_vec());
9242        assert!(
9243            validate_protocol_v2_fetch_command_request(&handshake, ObjectFormat::Sha1, &bad)
9244                .is_err()
9245        );
9246    }
9247
9248    #[test]
9249    fn protocol_v2_object_info_request_parses_encodes_and_validates() {
9250        let oid = ObjectId::from_hex(
9251            ObjectFormat::Sha1,
9252            "1111111111111111111111111111111111111111",
9253        )
9254        .expect("test operation should succeed");
9255        let request = ProtocolV2CommandRequest {
9256            command: "object-info".into(),
9257            capabilities: Vec::new(),
9258            arguments: vec![
9259                b"size".to_vec(),
9260                b"oid 1111111111111111111111111111111111111111".to_vec(),
9261            ],
9262        };
9263        let parsed =
9264            ProtocolV2ObjectInfoRequest::from_command_request(ObjectFormat::Sha1, &request)
9265                .expect("test operation should succeed");
9266        assert_eq!(
9267            parsed,
9268            ProtocolV2ObjectInfoRequest {
9269                size: true,
9270                oids: vec![oid],
9271            }
9272        );
9273        assert_eq!(
9274            parsed
9275                .to_command_request()
9276                .expect("test operation should succeed"),
9277            request
9278        );
9279
9280        let handshake = TransportHandshake {
9281            protocol: ProtocolVersion::V2,
9282            capabilities: vec![Capability {
9283                name: "object-info".into(),
9284                value: None,
9285            }],
9286        };
9287        assert_eq!(
9288            validate_protocol_v2_object_info_command_request(
9289                &handshake,
9290                ObjectFormat::Sha1,
9291                &request,
9292            )
9293            .expect("test operation should succeed"),
9294            parsed
9295        );
9296    }
9297
9298    #[test]
9299    fn protocol_v2_object_info_request_streams_round_trip() {
9300        let request = ProtocolV2ObjectInfoRequest {
9301            size: true,
9302            oids: vec![
9303                ObjectId::from_hex(
9304                    ObjectFormat::Sha1,
9305                    "1111111111111111111111111111111111111111",
9306                )
9307                .expect("test operation should succeed"),
9308            ],
9309        };
9310        let mut encoded = Vec::new();
9311        write_protocol_v2_object_info_request(&mut encoded, &request)
9312            .expect("test operation should succeed");
9313        encoded.extend_from_slice(b"tail");
9314
9315        let mut input = encoded.as_slice();
9316        assert_eq!(
9317            read_protocol_v2_object_info_request(ObjectFormat::Sha1, &mut input)
9318                .expect("test operation should succeed"),
9319            request
9320        );
9321        assert_eq!(input, b"tail");
9322    }
9323
9324    #[test]
9325    fn protocol_v2_object_info_request_rejects_malformed_arguments() {
9326        assert!(
9327            ProtocolV2ObjectInfoRequest::from_command_request(
9328                ObjectFormat::Sha1,
9329                &ProtocolV2CommandRequest {
9330                    command: "object-info".into(),
9331                    capabilities: Vec::new(),
9332                    arguments: vec![b"oid 1111111111111111111111111111111111111111".to_vec()],
9333                },
9334            )
9335            .is_err()
9336        );
9337        assert!(
9338            ProtocolV2ObjectInfoRequest::from_command_request(
9339                ObjectFormat::Sha1,
9340                &ProtocolV2CommandRequest {
9341                    command: "object-info".into(),
9342                    capabilities: Vec::new(),
9343                    arguments: vec![b"size".to_vec(), b"size".to_vec()],
9344                },
9345            )
9346            .is_err()
9347        );
9348        assert!(
9349            ProtocolV2ObjectInfoRequest::from_command_request(
9350                ObjectFormat::Sha1,
9351                &ProtocolV2CommandRequest {
9352                    command: "object-info".into(),
9353                    capabilities: Vec::new(),
9354                    arguments: vec![b"size".to_vec()],
9355                },
9356            )
9357            .is_err()
9358        );
9359        assert!(
9360            ProtocolV2ObjectInfoRequest::from_command_request(
9361                ObjectFormat::Sha1,
9362                &ProtocolV2CommandRequest {
9363                    command: "object-info".into(),
9364                    capabilities: Vec::new(),
9365                    arguments: vec![b"size".to_vec(), b"oid not-an-oid".to_vec()],
9366                },
9367            )
9368            .is_err()
9369        );
9370        assert!(
9371            validate_protocol_v2_object_info_command_request(
9372                &TransportHandshake {
9373                    protocol: ProtocolVersion::V2,
9374                    capabilities: Vec::new(),
9375                },
9376                ObjectFormat::Sha1,
9377                &ProtocolV2CommandRequest {
9378                    command: "object-info".into(),
9379                    capabilities: Vec::new(),
9380                    arguments: vec![
9381                        b"size".to_vec(),
9382                        b"oid 1111111111111111111111111111111111111111".to_vec(),
9383                    ],
9384                },
9385            )
9386            .is_err()
9387        );
9388    }
9389
9390    #[test]
9391    fn protocol_v2_command_request_classifies_known_and_unknown_commands() {
9392        let handshake = TransportHandshake {
9393            protocol: ProtocolVersion::V2,
9394            capabilities: vec![
9395                Capability {
9396                    name: "ls-refs".into(),
9397                    value: Some("unborn".into()),
9398                },
9399                Capability {
9400                    name: "fetch".into(),
9401                    value: Some("filter ref-in-want".into()),
9402                },
9403                Capability {
9404                    name: "object-info".into(),
9405                    value: None,
9406                },
9407                Capability {
9408                    name: "server-option".into(),
9409                    value: None,
9410                },
9411                Capability {
9412                    name: "server-info".into(),
9413                    value: Some("custom".into()),
9414                },
9415            ],
9416        };
9417        assert_eq!(
9418            classify_protocol_v2_command_request(
9419                &handshake,
9420                ObjectFormat::Sha1,
9421                &ProtocolV2CommandRequest {
9422                    command: "ls-refs".into(),
9423                    capabilities: Vec::new(),
9424                    arguments: vec![b"unborn".to_vec()],
9425                },
9426            )
9427            .expect("test operation should succeed"),
9428            ProtocolV2Command::LsRefs(ProtocolV2LsRefsRequest {
9429                unborn: true,
9430                ..ProtocolV2LsRefsRequest::default()
9431            })
9432        );
9433        assert_eq!(
9434            classify_protocol_v2_command_request(
9435                &handshake,
9436                ObjectFormat::Sha1,
9437                &ProtocolV2CommandRequest {
9438                    command: "fetch".into(),
9439                    capabilities: Vec::new(),
9440                    arguments: vec![
9441                        b"want-ref refs/heads/main".to_vec(),
9442                        b"filter blob:none".to_vec(),
9443                    ],
9444                },
9445            )
9446            .expect("test operation should succeed"),
9447            ProtocolV2Command::Fetch(ProtocolV2FetchRequest {
9448                want_refs: vec!["refs/heads/main".into()],
9449                filter: Some("blob:none".into()),
9450                ..ProtocolV2FetchRequest::default()
9451            })
9452        );
9453        assert_eq!(
9454            classify_protocol_v2_command_request(
9455                &handshake,
9456                ObjectFormat::Sha1,
9457                &ProtocolV2CommandRequest {
9458                    command: "object-info".into(),
9459                    capabilities: Vec::new(),
9460                    arguments: vec![
9461                        b"size".to_vec(),
9462                        b"oid 1111111111111111111111111111111111111111".to_vec(),
9463                    ],
9464                },
9465            )
9466            .expect("test operation should succeed"),
9467            ProtocolV2Command::ObjectInfo(ProtocolV2ObjectInfoRequest {
9468                size: true,
9469                oids: vec![
9470                    ObjectId::from_hex(
9471                        ObjectFormat::Sha1,
9472                        "1111111111111111111111111111111111111111",
9473                    )
9474                    .expect("test operation should succeed")
9475                ],
9476            })
9477        );
9478
9479        let unknown = ProtocolV2CommandRequest {
9480            command: "server-info".into(),
9481            capabilities: vec![Capability {
9482                name: "server-option".into(),
9483                value: Some("trace=true".into()),
9484            }],
9485            arguments: Vec::new(),
9486        };
9487        assert_eq!(
9488            classify_protocol_v2_command_request(&handshake, ObjectFormat::Sha1, &unknown)
9489                .expect("test operation should succeed"),
9490            ProtocolV2Command::Unknown(unknown)
9491        );
9492        assert!(
9493            classify_protocol_v2_command_request(
9494                &handshake,
9495                ObjectFormat::Sha1,
9496                &ProtocolV2CommandRequest {
9497                    command: "not-advertised".into(),
9498                    capabilities: Vec::new(),
9499                    arguments: Vec::new(),
9500                },
9501            )
9502            .is_err()
9503        );
9504    }
9505
9506    #[test]
9507    fn protocol_v2_session_request_classifies_streamed_command_and_done() {
9508        let handshake = TransportHandshake {
9509            protocol: ProtocolVersion::V2,
9510            capabilities: vec![
9511                Capability {
9512                    name: "ls-refs".into(),
9513                    value: Some("unborn".into()),
9514                },
9515                Capability {
9516                    name: "fetch".into(),
9517                    value: Some("filter ref-in-want".into()),
9518                },
9519            ],
9520        };
9521        let command = ProtocolV2Request::Command(ProtocolV2CommandRequest {
9522            command: "ls-refs".into(),
9523            capabilities: Vec::new(),
9524            arguments: vec![b"unborn".to_vec()],
9525        });
9526        assert_eq!(
9527            classify_protocol_v2_request(&handshake, ObjectFormat::Sha1, &command)
9528                .expect("test operation should succeed"),
9529            ProtocolV2SessionRequest::Command(ProtocolV2Command::LsRefs(ProtocolV2LsRefsRequest {
9530                unborn: true,
9531                ..ProtocolV2LsRefsRequest::default()
9532            }))
9533        );
9534        assert_eq!(
9535            classify_protocol_v2_request(&handshake, ObjectFormat::Sha1, &ProtocolV2Request::Done)
9536                .expect("test operation should succeed"),
9537            ProtocolV2SessionRequest::Done
9538        );
9539
9540        let mut encoded = Vec::new();
9541        write_protocol_v2_request(&mut encoded, &command).expect("test operation should succeed");
9542        write_protocol_v2_request(&mut encoded, &ProtocolV2Request::Done)
9543            .expect("test operation should succeed");
9544        encoded.extend_from_slice(b"tail");
9545
9546        let mut input = encoded.as_slice();
9547        assert_eq!(
9548            read_protocol_v2_session_request(&handshake, ObjectFormat::Sha1, &mut input)
9549                .expect("test operation should succeed"),
9550            ProtocolV2SessionRequest::Command(ProtocolV2Command::LsRefs(ProtocolV2LsRefsRequest {
9551                unborn: true,
9552                ..ProtocolV2LsRefsRequest::default()
9553            }))
9554        );
9555        assert_eq!(
9556            read_protocol_v2_session_request(&handshake, ObjectFormat::Sha1, &mut input)
9557                .expect("test operation should succeed"),
9558            ProtocolV2SessionRequest::Done
9559        );
9560        assert_eq!(input, b"tail");
9561    }
9562
9563    #[test]
9564    fn advertised_ref_parses_first_v0_capability_line() {
9565        let payload =
9566            b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 HEAD\0multi_ack symref=HEAD:refs/heads/main\n";
9567        let advertisement = parse_ref_advertisement(ObjectFormat::Sha1, payload)
9568            .expect("test operation should succeed");
9569        assert_eq!(
9570            advertisement.oid,
9571            ObjectId::from_hex(
9572                ObjectFormat::Sha1,
9573                "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"
9574            )
9575            .expect("test operation should succeed")
9576        );
9577        assert_eq!(advertisement.name, "HEAD");
9578        assert_eq!(
9579            advertisement.capabilities,
9580            vec![
9581                Capability {
9582                    name: "multi_ack".into(),
9583                    value: None,
9584                },
9585                Capability {
9586                    name: "symref".into(),
9587                    value: Some("HEAD:refs/heads/main".into()),
9588                },
9589            ]
9590        );
9591    }
9592
9593    #[test]
9594    fn advertised_ref_parses_lines_without_capabilities() {
9595        let advertisement = parse_ref_advertisement(
9596            ObjectFormat::Sha1,
9597            b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n",
9598        )
9599        .expect("test operation should succeed");
9600        assert_eq!(advertisement.name, "refs/heads/main");
9601        assert!(advertisement.capabilities.is_empty());
9602    }
9603
9604    #[test]
9605    fn advertised_ref_rejects_malformed_payloads() {
9606        assert!(
9607            parse_ref_advertisement(ObjectFormat::Sha1, b"not-an-oid refs/heads/main\n").is_err()
9608        );
9609        assert!(
9610            parse_ref_advertisement(
9611                ObjectFormat::Sha1,
9612                b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n"
9613            )
9614            .is_err()
9615        );
9616    }
9617
9618    #[test]
9619    fn advertised_refs_parse_and_encode_stream() {
9620        let main = ObjectId::from_hex(
9621            ObjectFormat::Sha1,
9622            "1111111111111111111111111111111111111111",
9623        )
9624        .expect("test operation should succeed");
9625        let feature = ObjectId::from_hex(
9626            ObjectFormat::Sha1,
9627            "2222222222222222222222222222222222222222",
9628        )
9629        .expect("test operation should succeed");
9630        let frames = vec![
9631            PktLineFrame::Data(
9632                b"1111111111111111111111111111111111111111 HEAD\0multi_ack thin-pack agent=git/2.54.0\n"
9633                    .to_vec(),
9634            ),
9635            PktLineFrame::Data(
9636                b"2222222222222222222222222222222222222222 refs/heads/feature\n".to_vec(),
9637            ),
9638            PktLineFrame::Flush,
9639        ];
9640        let advertisements = parse_ref_advertisements(ObjectFormat::Sha1, &frames)
9641            .expect("test operation should succeed");
9642        assert_eq!(
9643            advertisements,
9644            vec![
9645                RefAdvertisement {
9646                    oid: main,
9647                    name: "HEAD".into(),
9648                    capabilities: vec![
9649                        Capability {
9650                            name: "multi_ack".into(),
9651                            value: None,
9652                        },
9653                        Capability {
9654                            name: "thin-pack".into(),
9655                            value: None,
9656                        },
9657                        Capability {
9658                            name: "agent".into(),
9659                            value: Some("git/2.54.0".into()),
9660                        },
9661                    ],
9662                },
9663                RefAdvertisement {
9664                    oid: feature,
9665                    name: "refs/heads/feature".into(),
9666                    capabilities: Vec::new(),
9667                },
9668            ]
9669        );
9670        assert_eq!(
9671            encode_ref_advertisements(&advertisements).expect("test operation should succeed"),
9672            frames
9673        );
9674        assert_eq!(
9675            parse_ref_advertisements(ObjectFormat::Sha1, &[PktLineFrame::Flush])
9676                .expect("test operation should succeed"),
9677            Vec::<RefAdvertisement>::new()
9678        );
9679    }
9680
9681    #[test]
9682    fn advertised_ref_set_parses_v1_version_refs_and_shallow() {
9683        let main = ObjectId::from_hex(
9684            ObjectFormat::Sha1,
9685            "1111111111111111111111111111111111111111",
9686        )
9687        .expect("test operation should succeed");
9688        let feature = ObjectId::from_hex(
9689            ObjectFormat::Sha1,
9690            "2222222222222222222222222222222222222222",
9691        )
9692        .expect("test operation should succeed");
9693        let shallow = ObjectId::from_hex(
9694            ObjectFormat::Sha1,
9695            "3333333333333333333333333333333333333333",
9696        )
9697        .expect("test operation should succeed");
9698        let frames = vec![
9699            PktLineFrame::Data(b"version 1\n".to_vec()),
9700            PktLineFrame::Data(
9701                b"1111111111111111111111111111111111111111 HEAD\0multi_ack symref=HEAD:refs/heads/main\n"
9702                    .to_vec(),
9703            ),
9704            PktLineFrame::Data(
9705                b"2222222222222222222222222222222222222222 refs/heads/feature\n".to_vec(),
9706            ),
9707            PktLineFrame::Data(b"shallow 3333333333333333333333333333333333333333\n".to_vec()),
9708            PktLineFrame::Flush,
9709        ];
9710
9711        let set = parse_ref_advertisement_set(ObjectFormat::Sha1, &frames)
9712            .expect("test operation should succeed");
9713        assert_eq!(set.protocol, ProtocolVersion::V1);
9714        assert_eq!(set.shallow, vec![shallow]);
9715        assert_eq!(
9716            set.refs,
9717            vec![
9718                RefAdvertisement {
9719                    oid: main,
9720                    name: "HEAD".into(),
9721                    capabilities: vec![
9722                        Capability {
9723                            name: "multi_ack".into(),
9724                            value: None,
9725                        },
9726                        Capability {
9727                            name: "symref".into(),
9728                            value: Some("HEAD:refs/heads/main".into()),
9729                        },
9730                    ],
9731                },
9732                RefAdvertisement {
9733                    oid: feature,
9734                    name: "refs/heads/feature".into(),
9735                    capabilities: Vec::new(),
9736                },
9737            ]
9738        );
9739        assert_eq!(
9740            parse_ref_advertisements(ObjectFormat::Sha1, &frames)
9741                .expect("test operation should succeed"),
9742            set.refs
9743        );
9744        assert_eq!(
9745            encode_ref_advertisement_set(&set).expect("test operation should succeed"),
9746            frames
9747        );
9748    }
9749
9750    #[test]
9751    fn advertised_refs_streams_round_trip() {
9752        let advertisements = vec![RefAdvertisement {
9753            oid: ObjectId::from_hex(
9754                ObjectFormat::Sha1,
9755                "1111111111111111111111111111111111111111",
9756            )
9757            .expect("test operation should succeed"),
9758            name: "HEAD".into(),
9759            capabilities: vec![Capability {
9760                name: "symref".into(),
9761                value: Some("HEAD:refs/heads/main".into()),
9762            }],
9763        }];
9764        let mut encoded = Vec::new();
9765        write_ref_advertisements(&mut encoded, &advertisements)
9766            .expect("test operation should succeed");
9767        encoded.extend_from_slice(b"tail");
9768
9769        let mut input = encoded.as_slice();
9770        assert_eq!(
9771            read_ref_advertisements(ObjectFormat::Sha1, &mut input)
9772                .expect("test operation should succeed"),
9773            advertisements
9774        );
9775        assert_eq!(input, b"tail");
9776    }
9777
9778    #[test]
9779    fn advertised_ref_set_streams_round_trip() {
9780        let set = RefAdvertisementSet {
9781            protocol: ProtocolVersion::V1,
9782            refs: vec![RefAdvertisement {
9783                oid: ObjectId::from_hex(
9784                    ObjectFormat::Sha1,
9785                    "1111111111111111111111111111111111111111",
9786                )
9787                .expect("test operation should succeed"),
9788                name: "HEAD".into(),
9789                capabilities: vec![Capability {
9790                    name: "symref".into(),
9791                    value: Some("HEAD:refs/heads/main".into()),
9792                }],
9793            }],
9794            shallow: vec![
9795                ObjectId::from_hex(
9796                    ObjectFormat::Sha1,
9797                    "2222222222222222222222222222222222222222",
9798                )
9799                .expect("test operation should succeed"),
9800            ],
9801        };
9802        let mut encoded = Vec::new();
9803        write_ref_advertisement_set(&mut encoded, &set).expect("test operation should succeed");
9804        encoded.extend_from_slice(b"tail");
9805
9806        let mut input = encoded.as_slice();
9807        assert_eq!(
9808            read_ref_advertisement_set(ObjectFormat::Sha1, &mut input)
9809                .expect("test operation should succeed"),
9810            set
9811        );
9812        assert_eq!(input, b"tail");
9813    }
9814
9815    #[test]
9816    fn advertised_refs_reject_malformed_streams() {
9817        assert!(
9818            parse_ref_advertisements(
9819                ObjectFormat::Sha1,
9820                &[PktLineFrame::Data(
9821                    b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),
9822                )],
9823            )
9824            .is_err()
9825        );
9826        assert!(
9827            parse_ref_advertisements(
9828                ObjectFormat::Sha1,
9829                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
9830            )
9831            .is_err()
9832        );
9833        assert!(parse_ref_advertisements(
9834            ObjectFormat::Sha1,
9835            &[
9836                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
9837                PktLineFrame::Data(
9838                    b"2222222222222222222222222222222222222222 refs/heads/main\0thin-pack\n"
9839                        .to_vec(),
9840                ),
9841                PktLineFrame::Flush,
9842            ],
9843        )
9844        .is_err());
9845        assert!(parse_ref_advertisement_set(
9846            ObjectFormat::Sha1,
9847            &[
9848                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
9849                PktLineFrame::Data(b"version 1\n".to_vec()),
9850                PktLineFrame::Flush,
9851            ],
9852        )
9853        .is_err());
9854        assert!(
9855            parse_ref_advertisement_set(
9856                ObjectFormat::Sha1,
9857                &[
9858                    PktLineFrame::Data(b"version 2\n".to_vec()),
9859                    PktLineFrame::Flush,
9860                ],
9861            )
9862            .is_err()
9863        );
9864        assert!(
9865            parse_ref_advertisement_set(
9866                ObjectFormat::Sha1,
9867                &[
9868                    PktLineFrame::Data(
9869                        b"shallow 1111111111111111111111111111111111111111\n".to_vec()
9870                    ),
9871                    PktLineFrame::Flush,
9872                ],
9873            )
9874            .is_err()
9875        );
9876        assert!(parse_ref_advertisement_set(
9877            ObjectFormat::Sha1,
9878            &[
9879                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
9880                PktLineFrame::Data(b"shallow not-an-oid\n".to_vec()),
9881                PktLineFrame::Flush,
9882            ],
9883        )
9884        .is_err());
9885        assert!(parse_ref_advertisement_set(
9886            ObjectFormat::Sha1,
9887            &[
9888                PktLineFrame::Data(b"1111111111111111111111111111111111111111 HEAD\n".to_vec(),),
9889                PktLineFrame::Data(b"shallow 2222222222222222222222222222222222222222\n".to_vec()),
9890                PktLineFrame::Data(
9891                    b"3333333333333333333333333333333333333333 refs/heads/main\n".to_vec(),
9892                ),
9893                PktLineFrame::Flush,
9894            ],
9895        )
9896        .is_err());
9897        assert!(
9898            encode_ref_advertisements(&[
9899                RefAdvertisement {
9900                    oid: ObjectId::from_hex(
9901                        ObjectFormat::Sha1,
9902                        "1111111111111111111111111111111111111111",
9903                    )
9904                    .expect("test operation should succeed"),
9905                    name: "HEAD".into(),
9906                    capabilities: Vec::new(),
9907                },
9908                RefAdvertisement {
9909                    oid: ObjectId::from_hex(
9910                        ObjectFormat::Sha1,
9911                        "2222222222222222222222222222222222222222",
9912                    )
9913                    .expect("test operation should succeed"),
9914                    name: "refs/heads/main".into(),
9915                    capabilities: vec![Capability {
9916                        name: "thin-pack".into(),
9917                        value: None,
9918                    }],
9919                },
9920            ])
9921            .is_err()
9922        );
9923        assert!(
9924            encode_ref_advertisement(&RefAdvertisement {
9925                oid: ObjectId::from_hex(
9926                    ObjectFormat::Sha1,
9927                    "1111111111111111111111111111111111111111",
9928                )
9929                .expect("test operation should succeed"),
9930                name: "bad ref".into(),
9931                capabilities: Vec::new(),
9932            })
9933            .is_err()
9934        );
9935        assert!(
9936            encode_ref_advertisement_set(&RefAdvertisementSet {
9937                protocol: ProtocolVersion::V2,
9938                refs: Vec::new(),
9939                shallow: Vec::new(),
9940            })
9941            .is_err()
9942        );
9943        assert!(
9944            encode_ref_advertisement_set(&RefAdvertisementSet {
9945                protocol: ProtocolVersion::V0,
9946                refs: Vec::new(),
9947                shallow: vec![
9948                    ObjectId::from_hex(
9949                        ObjectFormat::Sha1,
9950                        "1111111111111111111111111111111111111111",
9951                    )
9952                    .expect("test operation should succeed")
9953                ],
9954            })
9955            .is_err()
9956        );
9957    }
9958
9959    #[test]
9960    fn dumb_http_info_refs_parse_and_encode_records() {
9961        let main = ObjectId::from_hex(
9962            ObjectFormat::Sha1,
9963            "1111111111111111111111111111111111111111",
9964        )
9965        .expect("test operation should succeed");
9966        let tag = ObjectId::from_hex(
9967            ObjectFormat::Sha1,
9968            "2222222222222222222222222222222222222222",
9969        )
9970        .expect("test operation should succeed");
9971        let peeled = ObjectId::from_hex(
9972            ObjectFormat::Sha1,
9973            "3333333333333333333333333333333333333333",
9974        )
9975        .expect("test operation should succeed");
9976        let input = b"1111111111111111111111111111111111111111\trefs/heads/main\n2222222222222222222222222222222222222222\trefs/tags/v1.0\n3333333333333333333333333333333333333333\trefs/tags/v1.0^{}\n";
9977
9978        let records = parse_dumb_http_info_refs(ObjectFormat::Sha1, input)
9979            .expect("test operation should succeed");
9980        assert_eq!(
9981            records,
9982            vec![
9983                DumbHttpRefRecord {
9984                    oid: main,
9985                    name: "refs/heads/main".into(),
9986                    peeled: false,
9987                },
9988                DumbHttpRefRecord {
9989                    oid: tag,
9990                    name: "refs/tags/v1.0".into(),
9991                    peeled: false,
9992                },
9993                DumbHttpRefRecord {
9994                    oid: peeled,
9995                    name: "refs/tags/v1.0".into(),
9996                    peeled: true,
9997                },
9998            ]
9999        );
10000        assert_eq!(
10001            encode_dumb_http_info_refs(&records).expect("test operation should succeed"),
10002            input
10003        );
10004        assert_eq!(
10005            parse_dumb_http_info_refs(ObjectFormat::Sha1, b"")
10006                .expect("test operation should succeed"),
10007            Vec::<DumbHttpRefRecord>::new()
10008        );
10009    }
10010
10011    #[test]
10012    fn dumb_http_info_refs_streams_round_trip() {
10013        let records = vec![DumbHttpRefRecord {
10014            oid: ObjectId::from_hex(
10015                ObjectFormat::Sha1,
10016                "1111111111111111111111111111111111111111",
10017            )
10018            .expect("test operation should succeed"),
10019            name: "refs/heads/main".into(),
10020            peeled: false,
10021        }];
10022        let mut encoded = Vec::new();
10023        write_dumb_http_info_refs(&mut encoded, &records).expect("test operation should succeed");
10024        let mut input = encoded.as_slice();
10025        assert_eq!(
10026            read_dumb_http_info_refs(ObjectFormat::Sha1, &mut input)
10027                .expect("test operation should succeed"),
10028            records
10029        );
10030        assert!(input.is_empty());
10031    }
10032
10033    #[test]
10034    fn dumb_http_info_refs_reject_malformed_records() {
10035        assert!(
10036            parse_dumb_http_info_refs(
10037                ObjectFormat::Sha1,
10038                b"1111111111111111111111111111111111111111 refs/heads/main\n",
10039            )
10040            .is_err()
10041        );
10042        assert!(
10043            parse_dumb_http_info_refs(
10044                ObjectFormat::Sha1,
10045                b"1111111111111111111111111111111111111111\trefs/heads/main",
10046            )
10047            .is_err()
10048        );
10049        assert!(
10050            parse_dumb_http_info_refs(ObjectFormat::Sha1, b"not-an-oid\trefs/heads/main\n")
10051                .is_err()
10052        );
10053        assert!(
10054            parse_dumb_http_info_refs(
10055                ObjectFormat::Sha1,
10056                b"1111111111111111111111111111111111111111\tbad ref\n",
10057            )
10058            .is_err()
10059        );
10060        assert!(
10061            encode_dumb_http_info_refs(&[DumbHttpRefRecord {
10062                oid: ObjectId::from_hex(
10063                    ObjectFormat::Sha1,
10064                    "1111111111111111111111111111111111111111",
10065                )
10066                .expect("test operation should succeed"),
10067                name: "refs/tags/v1.0^{}".into(),
10068                peeled: false,
10069            }])
10070            .is_err()
10071        );
10072    }
10073
10074    #[test]
10075    fn dumb_http_alternates_parse_and_encode_locations() {
10076        let input = b"https://example.com/base.git/objects/\n../other.git/objects/\n";
10077        let alternates = parse_dumb_http_alternates(input).expect("test operation should succeed");
10078        assert_eq!(
10079            alternates,
10080            vec![
10081                "https://example.com/base.git/objects/".to_string(),
10082                "../other.git/objects/".to_string(),
10083            ]
10084        );
10085        assert_eq!(
10086            encode_dumb_http_alternates(&alternates).expect("test operation should succeed"),
10087            input
10088        );
10089        assert_eq!(
10090            parse_dumb_http_alternates(b"").expect("test operation should succeed"),
10091            Vec::<String>::new()
10092        );
10093    }
10094
10095    #[test]
10096    fn dumb_http_alternates_streams_round_trip() {
10097        let alternates = vec!["https://example.com/base.git/objects/".to_string()];
10098        let mut encoded = Vec::new();
10099        write_dumb_http_alternates(&mut encoded, &alternates)
10100            .expect("test operation should succeed");
10101        let mut input = encoded.as_slice();
10102        assert_eq!(
10103            read_dumb_http_alternates(&mut input).expect("test operation should succeed"),
10104            alternates
10105        );
10106        assert!(input.is_empty());
10107    }
10108
10109    #[test]
10110    fn dumb_http_alternates_reject_malformed_lines() {
10111        assert!(parse_dumb_http_alternates(b"https://example.com/base.git/objects/").is_err());
10112        assert!(parse_dumb_http_alternates(b"\n").is_err());
10113        assert!(parse_dumb_http_alternates(b"https://example.com/base.git/objects/\r\n").is_err());
10114        assert!(encode_dumb_http_alternates(&["bad\nalternate".to_string()]).is_err());
10115    }
10116
10117    #[test]
10118    fn dumb_http_packs_parse_and_encode_pack_records() {
10119        let first = ObjectId::from_hex(
10120            ObjectFormat::Sha1,
10121            "1111111111111111111111111111111111111111",
10122        )
10123        .expect("test operation should succeed");
10124        let second = ObjectId::from_hex(
10125            ObjectFormat::Sha1,
10126            "2222222222222222222222222222222222222222",
10127        )
10128        .expect("test operation should succeed");
10129        let input = b"P pack-1111111111111111111111111111111111111111.pack\nP pack-2222222222222222222222222222222222222222.pack\n";
10130        let records = parse_dumb_http_packs(ObjectFormat::Sha1, input)
10131            .expect("test operation should succeed");
10132        assert_eq!(
10133            records,
10134            vec![
10135                DumbHttpPackRecord { hash: first },
10136                DumbHttpPackRecord { hash: second },
10137            ]
10138        );
10139        assert_eq!(
10140            encode_dumb_http_packs(&records).expect("test operation should succeed"),
10141            input
10142        );
10143        assert_eq!(
10144            parse_dumb_http_packs(ObjectFormat::Sha1, b"").expect("test operation should succeed"),
10145            Vec::<DumbHttpPackRecord>::new()
10146        );
10147    }
10148
10149    #[test]
10150    fn dumb_http_packs_streams_round_trip() {
10151        let records = vec![DumbHttpPackRecord {
10152            hash: ObjectId::from_hex(
10153                ObjectFormat::Sha1,
10154                "1111111111111111111111111111111111111111",
10155            )
10156            .expect("test operation should succeed"),
10157        }];
10158        let mut encoded = Vec::new();
10159        write_dumb_http_packs(&mut encoded, &records).expect("test operation should succeed");
10160        let mut input = encoded.as_slice();
10161        assert_eq!(
10162            read_dumb_http_packs(ObjectFormat::Sha1, &mut input)
10163                .expect("test operation should succeed"),
10164            records
10165        );
10166        assert!(input.is_empty());
10167    }
10168
10169    #[test]
10170    fn dumb_http_packs_reject_malformed_records() {
10171        assert!(
10172            parse_dumb_http_packs(
10173                ObjectFormat::Sha1,
10174                b"P pack-1111111111111111111111111111111111111111.pack",
10175            )
10176            .is_err()
10177        );
10178        assert!(
10179            parse_dumb_http_packs(
10180                ObjectFormat::Sha1,
10181                b"pack-1111111111111111111111111111111111111111.pack\n",
10182            )
10183            .is_err()
10184        );
10185        assert!(parse_dumb_http_packs(ObjectFormat::Sha1, b"P pack-not-a-hash.pack\n",).is_err());
10186        assert!(
10187            parse_dumb_http_packs(
10188                ObjectFormat::Sha1,
10189                b"P pack-1111111111111111111111111111111111111111.idx\n",
10190            )
10191            .is_err()
10192        );
10193    }
10194
10195    #[test]
10196    fn upload_pack_features_parse_encode_and_validate_request() {
10197        let capabilities = vec![
10198            Capability {
10199                name: "multi_ack".into(),
10200                value: None,
10201            },
10202            Capability {
10203                name: "multi_ack_detailed".into(),
10204                value: None,
10205            },
10206            Capability {
10207                name: "no-done".into(),
10208                value: None,
10209            },
10210            Capability {
10211                name: "thin-pack".into(),
10212                value: None,
10213            },
10214            Capability {
10215                name: "side-band-64k".into(),
10216                value: None,
10217            },
10218            Capability {
10219                name: "ofs-delta".into(),
10220                value: None,
10221            },
10222            Capability {
10223                name: "shallow".into(),
10224                value: None,
10225            },
10226            Capability {
10227                name: "deepen-since".into(),
10228                value: None,
10229            },
10230            Capability {
10231                name: "deepen-not".into(),
10232                value: None,
10233            },
10234            Capability {
10235                name: "include-tag".into(),
10236                value: None,
10237            },
10238            Capability {
10239                name: "no-progress".into(),
10240                value: None,
10241            },
10242            Capability {
10243                name: "filter".into(),
10244                value: None,
10245            },
10246            Capability {
10247                name: "agent".into(),
10248                value: Some("git/2.54.0".into()),
10249            },
10250            Capability {
10251                name: "object-format".into(),
10252                value: Some("sha256".into()),
10253            },
10254            Capability {
10255                name: "symref".into(),
10256                value: Some("HEAD:refs/heads/main".into()),
10257            },
10258            Capability {
10259                name: "custom".into(),
10260                value: Some("value".into()),
10261            },
10262        ];
10263        let features =
10264            parse_upload_pack_features(&capabilities).expect("test operation should succeed");
10265        assert_eq!(
10266            features,
10267            UploadPackFeatures {
10268                multi_ack: true,
10269                multi_ack_detailed: true,
10270                no_done: true,
10271                thin_pack: true,
10272                side_band_64k: true,
10273                ofs_delta: true,
10274                shallow: true,
10275                deepen_since: true,
10276                deepen_not: true,
10277                include_tag: true,
10278                no_progress: true,
10279                filter: true,
10280                agent: Some("git/2.54.0".into()),
10281                object_format: Some(ObjectFormat::Sha256),
10282                symrefs: vec!["HEAD:refs/heads/main".into()],
10283                unknown: vec![Capability {
10284                    name: "custom".into(),
10285                    value: Some("value".into()),
10286                }],
10287                ..UploadPackFeatures::default()
10288            }
10289        );
10290        assert_eq!(
10291            encode_upload_pack_features(&features).expect("test operation should succeed"),
10292            capabilities
10293        );
10294
10295        let request = UploadPackRequest {
10296            wants: vec![
10297                ObjectId::from_hex(
10298                    ObjectFormat::Sha1,
10299                    "1111111111111111111111111111111111111111",
10300                )
10301                .expect("test operation should succeed"),
10302            ],
10303            capabilities: vec![
10304                Capability {
10305                    name: "multi_ack_detailed".into(),
10306                    value: None,
10307                },
10308                Capability {
10309                    name: "thin-pack".into(),
10310                    value: None,
10311                },
10312                Capability {
10313                    name: "side-band-64k".into(),
10314                    value: None,
10315                },
10316                Capability {
10317                    name: "ofs-delta".into(),
10318                    value: None,
10319                },
10320                Capability {
10321                    name: "include-tag".into(),
10322                    value: None,
10323                },
10324                Capability {
10325                    name: "agent".into(),
10326                    value: Some("sley".into()),
10327                },
10328            ],
10329            shallow: vec![
10330                ObjectId::from_hex(
10331                    ObjectFormat::Sha1,
10332                    "2222222222222222222222222222222222222222",
10333                )
10334                .expect("test operation should succeed"),
10335            ],
10336            deepen: Some(5),
10337            deepen_since: Some(1_710_000_000),
10338            deepen_not: vec!["refs/tags/base".into()],
10339            filter: Some("blob:none".into()),
10340        };
10341        validate_upload_pack_request_features(&features, &request)
10342            .expect("test operation should succeed");
10343    }
10344
10345    #[test]
10346    fn upload_pack_features_reject_invalid_requests() {
10347        let want = ObjectId::from_hex(
10348            ObjectFormat::Sha1,
10349            "1111111111111111111111111111111111111111",
10350        )
10351        .expect("test operation should succeed");
10352        let features = UploadPackFeatures {
10353            thin_pack: true,
10354            side_band: true,
10355            ..UploadPackFeatures::default()
10356        };
10357
10358        assert!(
10359            validate_upload_pack_request_features(
10360                &features,
10361                &UploadPackRequest {
10362                    wants: vec![want],
10363                    capabilities: vec![Capability {
10364                        name: "ofs-delta".into(),
10365                        value: None,
10366                    }],
10367                    ..UploadPackRequest::default()
10368                },
10369            )
10370            .is_err()
10371        );
10372        assert!(
10373            validate_upload_pack_request_features(
10374                &features,
10375                &UploadPackRequest {
10376                    wants: vec![want],
10377                    shallow: vec![want],
10378                    ..UploadPackRequest::default()
10379                },
10380            )
10381            .is_err()
10382        );
10383        assert!(
10384            validate_upload_pack_request_features(
10385                &features,
10386                &UploadPackRequest {
10387                    wants: vec![want],
10388                    filter: Some("blob:none".into()),
10389                    ..UploadPackRequest::default()
10390                },
10391            )
10392            .is_err()
10393        );
10394        assert!(
10395            validate_upload_pack_request_features(
10396                &UploadPackFeatures {
10397                    side_band: true,
10398                    side_band_64k: true,
10399                    ..UploadPackFeatures::default()
10400                },
10401                &UploadPackRequest {
10402                    wants: vec![want],
10403                    capabilities: vec![
10404                        Capability {
10405                            name: "side-band".into(),
10406                            value: None,
10407                        },
10408                        Capability {
10409                            name: "side-band-64k".into(),
10410                            value: None,
10411                        },
10412                    ],
10413                    ..UploadPackRequest::default()
10414                },
10415            )
10416            .is_err()
10417        );
10418
10419        assert!(
10420            parse_upload_pack_features(&[
10421                Capability {
10422                    name: "thin-pack".into(),
10423                    value: None,
10424                },
10425                Capability {
10426                    name: "thin-pack".into(),
10427                    value: None,
10428                },
10429            ])
10430            .is_err()
10431        );
10432        assert!(
10433            encode_upload_pack_features(&UploadPackFeatures {
10434                unknown: vec![Capability {
10435                    name: "filter".into(),
10436                    value: None,
10437                }],
10438                ..UploadPackFeatures::default()
10439            })
10440            .is_err()
10441        );
10442    }
10443
10444    #[test]
10445    fn upload_pack_raw_response_builder_filters_unknown_haves_and_builds_pack() {
10446        let want = ObjectId::from_hex(
10447            ObjectFormat::Sha1,
10448            "1111111111111111111111111111111111111111",
10449        )
10450        .expect("test operation should succeed");
10451        let known_have = ObjectId::from_hex(
10452            ObjectFormat::Sha1,
10453            "2222222222222222222222222222222222222222",
10454        )
10455        .expect("test operation should succeed");
10456        let unknown_have = ObjectId::from_hex(
10457            ObjectFormat::Sha1,
10458            "3333333333333333333333333333333333333333",
10459        )
10460        .expect("test operation should succeed");
10461        let existing = std::collections::HashSet::from([want, known_have]);
10462
10463        let response = build_upload_pack_raw_packfile_response(
10464            &UploadPackFeatures::default(),
10465            UploadPackRequest {
10466                wants: vec![want],
10467                ..UploadPackRequest::default()
10468            },
10469            [known_have, unknown_have],
10470            |oid| Ok(existing.contains(oid)),
10471            |wants, haves| {
10472                assert_eq!(wants, vec![want]);
10473                assert_eq!(haves, vec![known_have]);
10474                Ok(Some(b"PACKmock".to_vec()))
10475            },
10476        )
10477        .expect("test operation should succeed");
10478
10479        assert_eq!(
10480            response.acknowledgments,
10481            vec![UploadPackAcknowledgment::Nak]
10482        );
10483        assert_eq!(response.packfile, b"PACKmock");
10484    }
10485
10486    #[test]
10487    fn upload_pack_raw_response_builder_rejects_missing_want_and_empty_pack() {
10488        let want = ObjectId::from_hex(
10489            ObjectFormat::Sha1,
10490            "1111111111111111111111111111111111111111",
10491        )
10492        .expect("test operation should succeed");
10493
10494        assert!(
10495            build_upload_pack_raw_packfile_response(
10496                &UploadPackFeatures::default(),
10497                UploadPackRequest {
10498                    wants: vec![want],
10499                    ..UploadPackRequest::default()
10500                },
10501                Vec::<ObjectId>::new(),
10502                |_| Ok(false),
10503                |_, _| Ok(Some(b"PACKmock".to_vec())),
10504            )
10505            .is_err()
10506        );
10507
10508        assert!(
10509            build_upload_pack_raw_packfile_response(
10510                &UploadPackFeatures::default(),
10511                UploadPackRequest {
10512                    wants: vec![want],
10513                    ..UploadPackRequest::default()
10514                },
10515                Vec::<ObjectId>::new(),
10516                |_| Ok(true),
10517                |_, _| Ok(None),
10518            )
10519            .is_err()
10520        );
10521    }
10522
10523    #[test]
10524    fn upload_pack_request_parses_and_encodes_initial_fetch_request() {
10525        let want = ObjectId::from_hex(
10526            ObjectFormat::Sha1,
10527            "1111111111111111111111111111111111111111",
10528        )
10529        .expect("test operation should succeed");
10530        let second_want = ObjectId::from_hex(
10531            ObjectFormat::Sha1,
10532            "2222222222222222222222222222222222222222",
10533        )
10534        .expect("test operation should succeed");
10535        let shallow = ObjectId::from_hex(
10536            ObjectFormat::Sha1,
10537            "3333333333333333333333333333333333333333",
10538        )
10539        .expect("test operation should succeed");
10540        let frames = vec![
10541            PktLineFrame::Data(
10542                b"want 1111111111111111111111111111111111111111 multi_ack thin-pack agent=git/2.54.0\n"
10543                    .to_vec(),
10544            ),
10545            PktLineFrame::Data(b"want 2222222222222222222222222222222222222222\n".to_vec()),
10546            PktLineFrame::Data(b"shallow 3333333333333333333333333333333333333333\n".to_vec()),
10547            PktLineFrame::Data(b"deepen-since 1710000000\n".to_vec()),
10548            PktLineFrame::Data(b"deepen-not refs/tags/base\n".to_vec()),
10549            PktLineFrame::Data(b"filter blob:none\n".to_vec()),
10550            PktLineFrame::Flush,
10551        ];
10552        let request = parse_upload_pack_request(ObjectFormat::Sha1, &frames)
10553            .expect("test operation should succeed")
10554            .expect("test operation should succeed");
10555        assert_eq!(
10556            request,
10557            UploadPackRequest {
10558                wants: vec![want, second_want],
10559                capabilities: vec![
10560                    Capability {
10561                        name: "multi_ack".into(),
10562                        value: None,
10563                    },
10564                    Capability {
10565                        name: "thin-pack".into(),
10566                        value: None,
10567                    },
10568                    Capability {
10569                        name: "agent".into(),
10570                        value: Some("git/2.54.0".into()),
10571                    },
10572                ],
10573                shallow: vec![shallow],
10574                deepen: None,
10575                deepen_since: Some(1_710_000_000),
10576                deepen_not: vec!["refs/tags/base".into()],
10577                filter: Some("blob:none".into()),
10578            }
10579        );
10580        assert_eq!(
10581            encode_upload_pack_request(Some(&request)).expect("test operation should succeed"),
10582            frames
10583        );
10584        assert_eq!(
10585            parse_upload_pack_request(ObjectFormat::Sha1, &[PktLineFrame::Flush])
10586                .expect("test operation should succeed"),
10587            None
10588        );
10589        assert_eq!(
10590            encode_upload_pack_request(None).expect("test operation should succeed"),
10591            vec![PktLineFrame::Flush]
10592        );
10593    }
10594
10595    #[test]
10596    fn upload_pack_request_streams_round_trip() {
10597        let request = UploadPackRequest {
10598            wants: vec![
10599                ObjectId::from_hex(
10600                    ObjectFormat::Sha1,
10601                    "1111111111111111111111111111111111111111",
10602                )
10603                .expect("test operation should succeed"),
10604            ],
10605            capabilities: vec![Capability {
10606                name: "ofs-delta".into(),
10607                value: None,
10608            }],
10609            deepen: Some(10),
10610            ..UploadPackRequest::default()
10611        };
10612        let mut encoded = Vec::new();
10613        write_upload_pack_request(&mut encoded, Some(&request))
10614            .expect("test operation should succeed");
10615        encoded.extend_from_slice(b"tail");
10616
10617        let mut input = encoded.as_slice();
10618        assert_eq!(
10619            read_upload_pack_request(ObjectFormat::Sha1, &mut input)
10620                .expect("test operation should succeed"),
10621            Some(request)
10622        );
10623        assert_eq!(input, b"tail");
10624    }
10625
10626    #[test]
10627    fn upload_pack_request_rejects_malformed_requests() {
10628        assert!(
10629            parse_upload_pack_request(
10630                ObjectFormat::Sha1,
10631                &[PktLineFrame::Data(
10632                    b"want 1111111111111111111111111111111111111111\n".to_vec(),
10633                )],
10634            )
10635            .is_err()
10636        );
10637        assert!(
10638            parse_upload_pack_request(
10639                ObjectFormat::Sha1,
10640                &[
10641                    PktLineFrame::Data(
10642                        b"shallow 1111111111111111111111111111111111111111\n".to_vec(),
10643                    ),
10644                    PktLineFrame::Flush,
10645                ],
10646            )
10647            .is_err()
10648        );
10649        assert!(
10650            parse_upload_pack_request(
10651                ObjectFormat::Sha1,
10652                &[
10653                    PktLineFrame::Data(
10654                        b"want 1111111111111111111111111111111111111111 thin-pack\n".to_vec(),
10655                    ),
10656                    PktLineFrame::Data(
10657                        b"want 2222222222222222222222222222222222222222 ofs-delta\n".to_vec(),
10658                    ),
10659                    PktLineFrame::Flush,
10660                ],
10661            )
10662            .is_err()
10663        );
10664        assert!(parse_upload_pack_request(
10665            ObjectFormat::Sha1,
10666            &[
10667                PktLineFrame::Data(b"want 1111111111111111111111111111111111111111\n".to_vec(),),
10668                PktLineFrame::Data(b"deepen 1\n".to_vec()),
10669                PktLineFrame::Data(b"want 2222222222222222222222222222222222222222\n".to_vec()),
10670                PktLineFrame::Flush,
10671            ],
10672        )
10673        .is_err());
10674        assert!(parse_upload_pack_request(
10675            ObjectFormat::Sha1,
10676            &[
10677                PktLineFrame::Data(b"want 1111111111111111111111111111111111111111\n".to_vec(),),
10678                PktLineFrame::Data(b"filter blob:none\n".to_vec()),
10679                PktLineFrame::Data(b"filter tree:0\n".to_vec()),
10680                PktLineFrame::Flush,
10681            ],
10682        )
10683        .is_err());
10684        assert!(encode_upload_pack_request(Some(&UploadPackRequest::default())).is_err());
10685        assert!(
10686            encode_upload_pack_request(Some(&UploadPackRequest {
10687                wants: vec![
10688                    ObjectId::from_hex(
10689                        ObjectFormat::Sha1,
10690                        "1111111111111111111111111111111111111111",
10691                    )
10692                    .expect("test operation should succeed")
10693                ],
10694                deepen: Some(0),
10695                ..UploadPackRequest::default()
10696            }))
10697            .is_err()
10698        );
10699    }
10700
10701    #[test]
10702    fn upload_pack_shallow_update_parses_and_encodes_records() {
10703        let shallow = ObjectId::from_hex(
10704            ObjectFormat::Sha1,
10705            "1111111111111111111111111111111111111111",
10706        )
10707        .expect("test operation should succeed");
10708        let unshallow = ObjectId::from_hex(
10709            ObjectFormat::Sha1,
10710            "2222222222222222222222222222222222222222",
10711        )
10712        .expect("test operation should succeed");
10713        let frames = vec![
10714            PktLineFrame::Data(b"shallow 1111111111111111111111111111111111111111\n".to_vec()),
10715            PktLineFrame::Data(b"unshallow 2222222222222222222222222222222222222222\n".to_vec()),
10716            PktLineFrame::Flush,
10717        ];
10718        let entries = parse_upload_pack_shallow_update(ObjectFormat::Sha1, &frames)
10719            .expect("test operation should succeed");
10720        assert_eq!(
10721            entries,
10722            vec![
10723                ProtocolV2FetchShallowInfo::Shallow(shallow),
10724                ProtocolV2FetchShallowInfo::Unshallow(unshallow),
10725            ]
10726        );
10727        assert_eq!(
10728            encode_upload_pack_shallow_update(&entries).expect("test operation should succeed"),
10729            frames
10730        );
10731        assert_eq!(
10732            parse_upload_pack_shallow_update(ObjectFormat::Sha1, &[PktLineFrame::Flush])
10733                .expect("test operation should succeed"),
10734            Vec::<ProtocolV2FetchShallowInfo>::new()
10735        );
10736    }
10737
10738    #[test]
10739    fn upload_pack_shallow_update_streams_round_trip() {
10740        let entries = vec![ProtocolV2FetchShallowInfo::Shallow(
10741            ObjectId::from_hex(
10742                ObjectFormat::Sha1,
10743                "1111111111111111111111111111111111111111",
10744            )
10745            .expect("test operation should succeed"),
10746        )];
10747        let mut encoded = Vec::new();
10748        write_upload_pack_shallow_update(&mut encoded, &entries)
10749            .expect("test operation should succeed");
10750        encoded.extend_from_slice(b"tail");
10751
10752        let mut input = encoded.as_slice();
10753        assert_eq!(
10754            read_upload_pack_shallow_update(ObjectFormat::Sha1, &mut input)
10755                .expect("test operation should succeed"),
10756            entries
10757        );
10758        assert_eq!(input, b"tail");
10759    }
10760
10761    #[test]
10762    fn upload_pack_shallow_update_rejects_malformed_records() {
10763        assert!(
10764            parse_upload_pack_shallow_update(
10765                ObjectFormat::Sha1,
10766                &[PktLineFrame::Data(
10767                    b"shallow 1111111111111111111111111111111111111111\n".to_vec(),
10768                )],
10769            )
10770            .is_err()
10771        );
10772        assert!(
10773            parse_upload_pack_shallow_update(
10774                ObjectFormat::Sha1,
10775                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
10776            )
10777            .is_err()
10778        );
10779        assert!(
10780            parse_upload_pack_shallow_update(
10781                ObjectFormat::Sha1,
10782                &[
10783                    PktLineFrame::Data(
10784                        b"shallow 1111111111111111111111111111111111111111\n".to_vec(),
10785                    ),
10786                    PktLineFrame::Flush,
10787                    PktLineFrame::Data(
10788                        b"unshallow 2222222222222222222222222222222222222222\n".to_vec(),
10789                    ),
10790                ],
10791            )
10792            .is_err()
10793        );
10794        assert!(
10795            parse_upload_pack_shallow_update(
10796                ObjectFormat::Sha1,
10797                &[
10798                    PktLineFrame::Data(
10799                        b"unsupported 1111111111111111111111111111111111111111\n".to_vec(),
10800                    ),
10801                    PktLineFrame::Flush,
10802                ],
10803            )
10804            .is_err()
10805        );
10806    }
10807
10808    #[test]
10809    fn upload_pack_negotiation_request_parses_flush_and_done_rounds() {
10810        let have = ObjectId::from_hex(
10811            ObjectFormat::Sha1,
10812            "1111111111111111111111111111111111111111",
10813        )
10814        .expect("test operation should succeed");
10815        let second_have = ObjectId::from_hex(
10816            ObjectFormat::Sha1,
10817            "2222222222222222222222222222222222222222",
10818        )
10819        .expect("test operation should succeed");
10820        let flush_round = vec![
10821            PktLineFrame::Data(b"have 1111111111111111111111111111111111111111\n".to_vec()),
10822            PktLineFrame::Data(b"have 2222222222222222222222222222222222222222\n".to_vec()),
10823            PktLineFrame::Flush,
10824        ];
10825        let request = parse_upload_pack_negotiation_request(ObjectFormat::Sha1, &flush_round)
10826            .expect("test operation should succeed");
10827        assert_eq!(
10828            request,
10829            UploadPackNegotiationRequest {
10830                haves: vec![have, second_have],
10831                done: false,
10832            }
10833        );
10834        assert_eq!(
10835            encode_upload_pack_negotiation_request(&request)
10836                .expect("test operation should succeed"),
10837            flush_round
10838        );
10839
10840        let done_round = vec![
10841            PktLineFrame::Data(b"have 1111111111111111111111111111111111111111\n".to_vec()),
10842            PktLineFrame::Data(b"done\n".to_vec()),
10843        ];
10844        let request = parse_upload_pack_negotiation_request(ObjectFormat::Sha1, &done_round)
10845            .expect("test operation should succeed");
10846        assert_eq!(
10847            request,
10848            UploadPackNegotiationRequest {
10849                haves: vec![have],
10850                done: true,
10851            }
10852        );
10853        assert_eq!(
10854            encode_upload_pack_negotiation_request(&request)
10855                .expect("test operation should succeed"),
10856            done_round
10857        );
10858    }
10859
10860    #[test]
10861    fn upload_pack_negotiation_request_streams_round_trip() {
10862        let first = UploadPackNegotiationRequest {
10863            haves: vec![
10864                ObjectId::from_hex(
10865                    ObjectFormat::Sha1,
10866                    "1111111111111111111111111111111111111111",
10867                )
10868                .expect("test operation should succeed"),
10869            ],
10870            done: false,
10871        };
10872        let second = UploadPackNegotiationRequest {
10873            haves: Vec::new(),
10874            done: true,
10875        };
10876        let mut encoded = Vec::new();
10877        write_upload_pack_negotiation_request(&mut encoded, &first)
10878            .expect("test operation should succeed");
10879        write_upload_pack_negotiation_request(&mut encoded, &second)
10880            .expect("test operation should succeed");
10881        encoded.extend_from_slice(b"tail");
10882
10883        let mut input = encoded.as_slice();
10884        assert_eq!(
10885            read_upload_pack_negotiation_request(ObjectFormat::Sha1, &mut input)
10886                .expect("test operation should succeed"),
10887            first
10888        );
10889        assert_eq!(
10890            read_upload_pack_negotiation_request(ObjectFormat::Sha1, &mut input)
10891                .expect("test operation should succeed"),
10892            second
10893        );
10894        assert_eq!(input, b"tail");
10895    }
10896
10897    #[test]
10898    fn upload_pack_negotiation_request_rejects_malformed_rounds() {
10899        assert!(
10900            parse_upload_pack_negotiation_request(
10901                ObjectFormat::Sha1,
10902                &[PktLineFrame::Data(
10903                    b"have 1111111111111111111111111111111111111111\n".to_vec(),
10904                )],
10905            )
10906            .is_err()
10907        );
10908        assert!(
10909            parse_upload_pack_negotiation_request(
10910                ObjectFormat::Sha1,
10911                &[PktLineFrame::Data(
10912                    b"want 1111111111111111111111111111111111111111\n".to_vec(),
10913                )],
10914            )
10915            .is_err()
10916        );
10917        assert!(parse_upload_pack_negotiation_request(
10918            ObjectFormat::Sha1,
10919            &[
10920                PktLineFrame::Data(b"done\n".to_vec()),
10921                PktLineFrame::Data(b"have 1111111111111111111111111111111111111111\n".to_vec(),),
10922            ],
10923        )
10924        .is_err());
10925        assert!(
10926            parse_upload_pack_negotiation_request(
10927                ObjectFormat::Sha1,
10928                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
10929            )
10930            .is_err()
10931        );
10932    }
10933
10934    #[test]
10935    fn upload_pack_acknowledgments_parse_and_encode_statuses() {
10936        let oid = ObjectId::from_hex(
10937            ObjectFormat::Sha1,
10938            "1111111111111111111111111111111111111111",
10939        )
10940        .expect("test operation should succeed");
10941        assert_eq!(
10942            parse_upload_pack_acknowledgment(ObjectFormat::Sha1, b"NAK\n")
10943                .expect("test operation should succeed"),
10944            UploadPackAcknowledgment::Nak
10945        );
10946        for (payload, status) in [
10947            (
10948                b"ACK 1111111111111111111111111111111111111111\n".as_slice(),
10949                None,
10950            ),
10951            (
10952                b"ACK 1111111111111111111111111111111111111111 continue\n".as_slice(),
10953                Some(UploadPackAckStatus::Continue),
10954            ),
10955            (
10956                b"ACK 1111111111111111111111111111111111111111 common\n".as_slice(),
10957                Some(UploadPackAckStatus::Common),
10958            ),
10959            (
10960                b"ACK 1111111111111111111111111111111111111111 ready\n".as_slice(),
10961                Some(UploadPackAckStatus::Ready),
10962            ),
10963        ] {
10964            let acknowledgment = parse_upload_pack_acknowledgment(ObjectFormat::Sha1, payload)
10965                .expect("test operation should succeed");
10966            assert_eq!(
10967                acknowledgment,
10968                UploadPackAcknowledgment::Ack { oid, status }
10969            );
10970            assert_eq!(
10971                encode_upload_pack_acknowledgment(&acknowledgment)
10972                    .expect("test operation should succeed"),
10973                payload
10974            );
10975        }
10976    }
10977
10978    #[test]
10979    fn upload_pack_acknowledgments_stream_round_trip_and_reject_bad_lines() {
10980        let acknowledgment = UploadPackAcknowledgment::Ack {
10981            oid: ObjectId::from_hex(
10982                ObjectFormat::Sha1,
10983                "1111111111111111111111111111111111111111",
10984            )
10985            .expect("test operation should succeed"),
10986            status: Some(UploadPackAckStatus::Ready),
10987        };
10988        let mut encoded = Vec::new();
10989        write_upload_pack_acknowledgment(&mut encoded, &acknowledgment)
10990            .expect("test operation should succeed");
10991        encoded.extend_from_slice(b"tail");
10992
10993        let mut input = encoded.as_slice();
10994        assert_eq!(
10995            read_upload_pack_acknowledgment(ObjectFormat::Sha1, &mut input)
10996                .expect("test operation should succeed"),
10997            acknowledgment
10998        );
10999        assert_eq!(input, b"tail");
11000        assert!(parse_upload_pack_acknowledgment(ObjectFormat::Sha1, b"ACK not-an-oid\n").is_err());
11001        assert!(
11002            parse_upload_pack_acknowledgment(
11003                ObjectFormat::Sha1,
11004                b"ACK 1111111111111111111111111111111111111111 unknown\n",
11005            )
11006            .is_err()
11007        );
11008        assert!(
11009            parse_upload_pack_acknowledgment(
11010                ObjectFormat::Sha1,
11011                b"ACK 1111111111111111111111111111111111111111 ready extra\n",
11012            )
11013            .is_err()
11014        );
11015        assert!(
11016            parse_upload_pack_acknowledgment(ObjectFormat::Sha1, b"ERR remote died\n").is_err()
11017        );
11018        assert!(read_upload_pack_acknowledgment(ObjectFormat::Sha1, &mut &b"0000"[..]).is_err());
11019    }
11020
11021    #[test]
11022    fn upload_pack_packfile_response_parses_acknowledgments_and_sideband() {
11023        let oid = ObjectId::from_hex(
11024            ObjectFormat::Sha1,
11025            "1111111111111111111111111111111111111111",
11026        )
11027        .expect("test operation should succeed");
11028        let frames = vec![
11029            PktLineFrame::Data(b"ACK 1111111111111111111111111111111111111111 common\n".to_vec()),
11030            PktLineFrame::Data(b"NAK\n".to_vec()),
11031            PktLineFrame::Data(b"\x01PACK".to_vec()),
11032            PktLineFrame::Data(b"\x02counting objects\n".to_vec()),
11033            PktLineFrame::Data(b"\x01 bytes".to_vec()),
11034            PktLineFrame::Flush,
11035        ];
11036        let response = parse_upload_pack_packfile_response(ObjectFormat::Sha1, &frames)
11037            .expect("test operation should succeed");
11038        assert_eq!(
11039            response,
11040            UploadPackPackfileResponse {
11041                acknowledgments: vec![
11042                    UploadPackAcknowledgment::Ack {
11043                        oid,
11044                        status: Some(UploadPackAckStatus::Common),
11045                    },
11046                    UploadPackAcknowledgment::Nak,
11047                ],
11048                sideband: vec![
11049                    SideBandPacket {
11050                        channel: SideBandChannel::Data,
11051                        data: b"PACK".to_vec(),
11052                    },
11053                    SideBandPacket {
11054                        channel: SideBandChannel::Progress,
11055                        data: b"counting objects\n".to_vec(),
11056                    },
11057                    SideBandPacket {
11058                        channel: SideBandChannel::Data,
11059                        data: b" bytes".to_vec(),
11060                    },
11061                ],
11062            }
11063        );
11064        assert_eq!(
11065            demux_upload_pack_packfile_response(&response).expect("test operation should succeed"),
11066            SideBandDemux {
11067                data: b"PACK bytes".to_vec(),
11068                progress: vec![b"counting objects\n".to_vec()],
11069            }
11070        );
11071        assert_eq!(
11072            encode_upload_pack_packfile_response(&response).expect("test operation should succeed"),
11073            frames
11074        );
11075    }
11076
11077    #[test]
11078    fn upload_pack_packfile_response_streams_round_trip() {
11079        let response = UploadPackPackfileResponse {
11080            acknowledgments: vec![UploadPackAcknowledgment::Nak],
11081            sideband: vec![SideBandPacket {
11082                channel: SideBandChannel::Data,
11083                data: b"PACK".to_vec(),
11084            }],
11085        };
11086        let mut encoded = Vec::new();
11087        write_upload_pack_packfile_response(&mut encoded, &response)
11088            .expect("test operation should succeed");
11089        encoded.extend_from_slice(b"tail");
11090
11091        let mut input = encoded.as_slice();
11092        assert_eq!(
11093            read_upload_pack_packfile_response(ObjectFormat::Sha1, &mut input)
11094                .expect("test operation should succeed"),
11095            response
11096        );
11097        assert_eq!(input, b"tail");
11098    }
11099
11100    #[test]
11101    fn upload_pack_packfile_response_rejects_malformed_streams() {
11102        assert!(
11103            parse_upload_pack_packfile_response(
11104                ObjectFormat::Sha1,
11105                &[PktLineFrame::Data(b"NAK\n".to_vec())],
11106            )
11107            .is_err()
11108        );
11109        assert!(
11110            parse_upload_pack_packfile_response(
11111                ObjectFormat::Sha1,
11112                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
11113            )
11114            .is_err()
11115        );
11116        assert!(
11117            parse_upload_pack_packfile_response(
11118                ObjectFormat::Sha1,
11119                &[
11120                    PktLineFrame::Data(b"\x01PACK".to_vec()),
11121                    PktLineFrame::Data(
11122                        b"ACK 1111111111111111111111111111111111111111 common\n".to_vec()
11123                    ),
11124                    PktLineFrame::Flush,
11125                ],
11126            )
11127            .is_err()
11128        );
11129        assert!(
11130            parse_upload_pack_packfile_response(
11131                ObjectFormat::Sha1,
11132                &[
11133                    PktLineFrame::Data(b"NAK\n".to_vec()),
11134                    PktLineFrame::Flush,
11135                    PktLineFrame::Data(b"\x01PACK".to_vec()),
11136                ],
11137            )
11138            .is_err()
11139        );
11140        assert!(
11141            parse_upload_pack_packfile_response(
11142                ObjectFormat::Sha1,
11143                &[
11144                    PktLineFrame::Data(b"NAK\n".to_vec()),
11145                    PktLineFrame::Data(b"\x04bad".to_vec()),
11146                    PktLineFrame::Flush,
11147                ],
11148            )
11149            .is_err()
11150        );
11151    }
11152
11153    #[test]
11154    fn upload_pack_raw_packfile_response_parses_acknowledgments_and_raw_pack() {
11155        let oid = ObjectId::from_hex(
11156            ObjectFormat::Sha1,
11157            "1111111111111111111111111111111111111111",
11158        )
11159        .expect("test operation should succeed");
11160        let response = UploadPackRawPackfileResponse {
11161            acknowledgments: vec![
11162                UploadPackAcknowledgment::Ack {
11163                    oid,
11164                    status: Some(UploadPackAckStatus::Common),
11165                },
11166                UploadPackAcknowledgment::Nak,
11167            ],
11168            packfile: b"PACK\x00\x00\x00\x02raw-bytes".to_vec(),
11169        };
11170        let encoded = encode_upload_pack_raw_packfile_response(&response)
11171            .expect("test operation should succeed");
11172        assert_eq!(
11173            parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &encoded)
11174                .expect("test operation should succeed"),
11175            response
11176        );
11177    }
11178
11179    #[test]
11180    fn upload_pack_raw_packfile_response_streams_round_trip() {
11181        let response = UploadPackRawPackfileResponse {
11182            acknowledgments: vec![UploadPackAcknowledgment::Nak],
11183            packfile: b"PACK\x00\x00\x00\x02raw-bytes".to_vec(),
11184        };
11185        let mut encoded = Vec::new();
11186        write_upload_pack_raw_packfile_response(&mut encoded, &response)
11187            .expect("test operation should succeed");
11188        assert_eq!(
11189            encoded,
11190            encode_upload_pack_raw_packfile_response(&response)
11191                .expect("test operation should succeed")
11192        );
11193
11194        let mut input = encoded.as_slice();
11195        assert_eq!(
11196            read_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &mut input)
11197                .expect("test operation should succeed"),
11198            response
11199        );
11200        assert!(input.is_empty());
11201    }
11202
11203    #[test]
11204    fn upload_pack_raw_packfile_response_rejects_malformed_streams() {
11205        let ack = PktLineFrame::data(b"NAK\n".to_vec())
11206            .expect("test operation should succeed")
11207            .try_encode()
11208            .expect("test operation should succeed");
11209        let bad_ack = PktLineFrame::data(b"ACK not-an-oid\n".to_vec())
11210            .expect("test operation should succeed")
11211            .try_encode()
11212            .expect("test operation should succeed");
11213        let non_ack =
11214            PktLineFrame::data(b"have 1111111111111111111111111111111111111111\n".to_vec())
11215                .expect("test operation should succeed")
11216                .try_encode()
11217                .expect("test operation should succeed");
11218        let mut garbage_after_ack = ack.clone();
11219        garbage_after_ack.extend_from_slice(b"garbage");
11220
11221        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, b"").is_err());
11222        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &ack).is_err());
11223        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &bad_ack).is_err());
11224        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, b"0000PACK").is_err());
11225        assert!(parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &non_ack).is_err());
11226        assert!(
11227            parse_upload_pack_raw_packfile_response(ObjectFormat::Sha1, &garbage_after_ack)
11228                .is_err()
11229        );
11230        assert!(
11231            encode_upload_pack_raw_packfile_response(&UploadPackRawPackfileResponse {
11232                acknowledgments: vec![UploadPackAcknowledgment::Nak],
11233                packfile: Vec::new(),
11234            })
11235            .is_err()
11236        );
11237        assert!(
11238            encode_upload_pack_raw_packfile_response(&UploadPackRawPackfileResponse {
11239                acknowledgments: Vec::new(),
11240                packfile: b"not-a-pack".to_vec(),
11241            })
11242            .is_err()
11243        );
11244    }
11245
11246    #[test]
11247    fn upload_pack_request_encodes_deepen_request() {
11248        // A `--depth 1` clone over smart-HTTP v1: the `want` line carries the
11249        // capabilities, the client's existing shallow boundary is replayed as a
11250        // `shallow` line, and `deepen 1` requests the truncation. Built as raw
11251        // pkt-line bytes so the 4-hex length prefixes are exercised.
11252        let want = ObjectId::from_hex(
11253            ObjectFormat::Sha1,
11254            "1111111111111111111111111111111111111111",
11255        )
11256        .expect("test operation should succeed");
11257        let boundary = ObjectId::from_hex(
11258            ObjectFormat::Sha1,
11259            "2222222222222222222222222222222222222222",
11260        )
11261        .expect("test operation should succeed");
11262        let request = UploadPackRequest {
11263            wants: vec![want],
11264            capabilities: vec![Capability {
11265                name: "shallow".into(),
11266                value: None,
11267            }],
11268            shallow: vec![boundary],
11269            deepen: Some(1),
11270            ..UploadPackRequest::default()
11271        };
11272        let mut encoded = Vec::new();
11273        write_upload_pack_request(&mut encoded, Some(&request))
11274            .expect("test operation should succeed");
11275        let mut expected = Vec::new();
11276        expected.extend_from_slice(b"003awant 1111111111111111111111111111111111111111 shallow\n");
11277        expected.extend_from_slice(b"0035shallow 2222222222222222222222222222222222222222\n");
11278        expected.extend_from_slice(b"000ddeepen 1\n");
11279        expected.extend_from_slice(b"0000");
11280        assert_eq!(encoded, expected);
11281    }
11282
11283    #[test]
11284    fn upload_pack_shallow_info_response_parses_shallow_unshallow_and_pack() {
11285        // The smart-HTTP v1 deepen response: a shallow-info section (one
11286        // `shallow` and one `unshallow` line) terminated by a flush, then the
11287        // NAK and the raw packfile. Hand-built pkt-lines (mind the lengths).
11288        let shallow = ObjectId::from_hex(
11289            ObjectFormat::Sha1,
11290            "1111111111111111111111111111111111111111",
11291        )
11292        .expect("test operation should succeed");
11293        let unshallow = ObjectId::from_hex(
11294            ObjectFormat::Sha1,
11295            "2222222222222222222222222222222222222222",
11296        )
11297        .expect("test operation should succeed");
11298        let mut input = Vec::new();
11299        input.extend_from_slice(b"0035shallow 1111111111111111111111111111111111111111\n");
11300        input.extend_from_slice(b"0037unshallow 2222222222222222222222222222222222222222\n");
11301        input.extend_from_slice(b"0000"); // shallow-info terminator
11302        input.extend_from_slice(b"0008NAK\n");
11303        input.extend_from_slice(b"PACK\x00\x00\x00\x02raw-bytes");
11304
11305        let (entries, response) =
11306            parse_upload_pack_shallow_info_and_raw_packfile_response(ObjectFormat::Sha1, &input)
11307                .expect("test operation should succeed");
11308        assert_eq!(
11309            entries,
11310            vec![
11311                ProtocolV2FetchShallowInfo::Shallow(shallow),
11312                ProtocolV2FetchShallowInfo::Unshallow(unshallow),
11313            ]
11314        );
11315        assert_eq!(
11316            response,
11317            UploadPackRawPackfileResponse {
11318                acknowledgments: vec![UploadPackAcknowledgment::Nak],
11319                packfile: b"PACK\x00\x00\x00\x02raw-bytes".to_vec(),
11320            }
11321        );
11322
11323        // The reader entry point yields the same result over a stream.
11324        let mut stream = input.as_slice();
11325        let (read_entries, read_response) =
11326            read_upload_pack_shallow_info_and_raw_packfile_response(
11327                ObjectFormat::Sha1,
11328                &mut stream,
11329            )
11330            .expect("test operation should succeed");
11331        assert_eq!(read_entries, entries);
11332        assert_eq!(read_response, response);
11333    }
11334
11335    #[test]
11336    fn upload_pack_shallow_info_response_handles_empty_shallow_section() {
11337        // A deepen request that creates no boundary change still gets an empty
11338        // shallow-info section (a bare flush) before the NAK + pack.
11339        let mut input = Vec::new();
11340        input.extend_from_slice(b"0000"); // empty shallow-info
11341        input.extend_from_slice(b"0008NAK\n");
11342        input.extend_from_slice(b"PACK\x00\x00\x00\x02raw-bytes");
11343
11344        let (entries, response) =
11345            parse_upload_pack_shallow_info_and_raw_packfile_response(ObjectFormat::Sha1, &input)
11346                .expect("test operation should succeed");
11347        assert!(entries.is_empty());
11348        assert_eq!(
11349            response.acknowledgments,
11350            vec![UploadPackAcknowledgment::Nak]
11351        );
11352        assert!(response.packfile.starts_with(b"PACK"));
11353    }
11354
11355    #[test]
11356    fn upload_pack_shallow_info_response_rejects_malformed_sections() {
11357        // Truncated section (no terminating flush before EOF).
11358        let truncated = b"0035shallow 1111111111111111111111111111111111111111\n".to_vec();
11359        assert!(
11360            parse_upload_pack_shallow_info_and_raw_packfile_response(
11361                ObjectFormat::Sha1,
11362                &truncated
11363            )
11364            .is_err()
11365        );
11366        // A non-flush control packet inside the shallow-info section.
11367        let mut delimiter_section = Vec::new();
11368        delimiter_section.extend_from_slice(b"0001"); // delimiter, not a flush
11369        assert!(
11370            parse_upload_pack_shallow_info_section(ObjectFormat::Sha1, &delimiter_section).is_err()
11371        );
11372        // A non-shallow data line inside the section.
11373        let mut bad_line = Vec::new();
11374        bad_line.extend_from_slice(b"0008NAK\n");
11375        assert!(parse_upload_pack_shallow_info_section(ObjectFormat::Sha1, &bad_line).is_err());
11376        // Valid shallow-info but a missing packfile afterwards.
11377        let mut no_pack = Vec::new();
11378        no_pack.extend_from_slice(b"0000"); // empty shallow-info
11379        no_pack.extend_from_slice(b"0008NAK\n");
11380        assert!(
11381            parse_upload_pack_shallow_info_and_raw_packfile_response(ObjectFormat::Sha1, &no_pack)
11382                .is_err()
11383        );
11384    }
11385
11386    #[test]
11387    fn receive_pack_request_parses_and_encodes_commands() {
11388        let old_id = ObjectId::from_hex(
11389            ObjectFormat::Sha1,
11390            "1111111111111111111111111111111111111111",
11391        )
11392        .expect("test operation should succeed");
11393        let new_id = ObjectId::from_hex(
11394            ObjectFormat::Sha1,
11395            "2222222222222222222222222222222222222222",
11396        )
11397        .expect("test operation should succeed");
11398        let delete_old_id = ObjectId::from_hex(
11399            ObjectFormat::Sha1,
11400            "3333333333333333333333333333333333333333",
11401        )
11402        .expect("test operation should succeed");
11403        let zero = ObjectId::from_hex(
11404            ObjectFormat::Sha1,
11405            "0000000000000000000000000000000000000000",
11406        )
11407        .expect("test operation should succeed");
11408        let shallow = ObjectId::from_hex(
11409            ObjectFormat::Sha1,
11410            "4444444444444444444444444444444444444444",
11411        )
11412        .expect("test operation should succeed");
11413        let frames = vec![
11414            PktLineFrame::Data(b"shallow 4444444444444444444444444444444444444444\n".to_vec()),
11415            PktLineFrame::Data(
11416                b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\0report-status side-band-64k agent=git/2.54.0\n"
11417                    .to_vec(),
11418            ),
11419            PktLineFrame::Data(
11420                b"3333333333333333333333333333333333333333 0000000000000000000000000000000000000000 refs/heads/old\n"
11421                    .to_vec(),
11422            ),
11423            PktLineFrame::Flush,
11424        ];
11425        let request = parse_receive_pack_request(ObjectFormat::Sha1, &frames)
11426            .expect("test operation should succeed");
11427        assert_eq!(
11428            request,
11429            ReceivePackRequest {
11430                shallow: vec![shallow],
11431                commands: vec![
11432                    ReceivePackCommand {
11433                        old_id,
11434                        new_id,
11435                        name: "refs/heads/main".into(),
11436                    },
11437                    ReceivePackCommand {
11438                        old_id: delete_old_id,
11439                        new_id: zero,
11440                        name: "refs/heads/old".into(),
11441                    },
11442                ],
11443                capabilities: vec![
11444                    Capability {
11445                        name: "report-status".into(),
11446                        value: None,
11447                    },
11448                    Capability {
11449                        name: "side-band-64k".into(),
11450                        value: None,
11451                    },
11452                    Capability {
11453                        name: "agent".into(),
11454                        value: Some("git/2.54.0".into()),
11455                    },
11456                ],
11457            }
11458        );
11459        assert_eq!(
11460            encode_receive_pack_request(&request).expect("test operation should succeed"),
11461            frames
11462        );
11463        assert_eq!(
11464            parse_receive_pack_request(ObjectFormat::Sha1, &[PktLineFrame::Flush])
11465                .expect("test operation should succeed"),
11466            ReceivePackRequest::default()
11467        );
11468    }
11469
11470    #[test]
11471    fn receive_pack_request_streams_round_trip() {
11472        let request = ReceivePackRequest {
11473            commands: vec![ReceivePackCommand {
11474                old_id: ObjectId::from_hex(
11475                    ObjectFormat::Sha1,
11476                    "0000000000000000000000000000000000000000",
11477                )
11478                .expect("test operation should succeed"),
11479                new_id: ObjectId::from_hex(
11480                    ObjectFormat::Sha1,
11481                    "1111111111111111111111111111111111111111",
11482                )
11483                .expect("test operation should succeed"),
11484                name: "refs/heads/main".into(),
11485            }],
11486            capabilities: vec![Capability {
11487                name: "report-status".into(),
11488                value: None,
11489            }],
11490            ..ReceivePackRequest::default()
11491        };
11492        let mut encoded = Vec::new();
11493        write_receive_pack_request(&mut encoded, &request).expect("test operation should succeed");
11494        encoded.extend_from_slice(b"PACK");
11495
11496        let mut input = encoded.as_slice();
11497        assert_eq!(
11498            read_receive_pack_request(ObjectFormat::Sha1, &mut input)
11499                .expect("test operation should succeed"),
11500            request
11501        );
11502        assert_eq!(input, b"PACK");
11503    }
11504
11505    #[test]
11506    fn receive_pack_request_rejects_malformed_commands() {
11507        assert!(
11508            parse_receive_pack_request(
11509                ObjectFormat::Sha1,
11510                &[PktLineFrame::Data(
11511                    b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\n"
11512                        .to_vec(),
11513                )],
11514            )
11515            .is_err()
11516        );
11517        assert!(
11518            parse_receive_pack_request(
11519                ObjectFormat::Sha1,
11520                &[
11521                    PktLineFrame::Data(
11522                        b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\n"
11523                            .to_vec(),
11524                    ),
11525                    PktLineFrame::Data(
11526                        b"shallow 3333333333333333333333333333333333333333\n".to_vec(),
11527                    ),
11528                    PktLineFrame::Flush,
11529                ],
11530            )
11531            .is_err()
11532        );
11533        assert!(
11534            parse_receive_pack_request(
11535                ObjectFormat::Sha1,
11536                &[
11537                    PktLineFrame::Data(
11538                        b"1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main\0report-status\n"
11539                            .to_vec(),
11540                    ),
11541                    PktLineFrame::Data(
11542                        b"3333333333333333333333333333333333333333 4444444444444444444444444444444444444444 refs/heads/next\0side-band-64k\n"
11543                            .to_vec(),
11544                    ),
11545                    PktLineFrame::Flush,
11546                ],
11547            )
11548            .is_err()
11549        );
11550        assert!(
11551            parse_receive_pack_request(
11552                ObjectFormat::Sha1,
11553                &[
11554                    PktLineFrame::Data(
11555                        b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec(),
11556                    ),
11557                    PktLineFrame::Flush,
11558                ],
11559            )
11560            .is_err()
11561        );
11562        assert!(
11563            encode_receive_pack_request(&ReceivePackRequest {
11564                shallow: vec![
11565                    ObjectId::from_hex(
11566                        ObjectFormat::Sha1,
11567                        "1111111111111111111111111111111111111111",
11568                    )
11569                    .expect("test operation should succeed")
11570                ],
11571                ..ReceivePackRequest::default()
11572            })
11573            .is_err()
11574        );
11575        assert!(
11576            encode_receive_pack_request(&ReceivePackRequest {
11577                commands: vec![ReceivePackCommand {
11578                    old_id: ObjectId::from_hex(
11579                        ObjectFormat::Sha1,
11580                        "1111111111111111111111111111111111111111",
11581                    )
11582                    .expect("test operation should succeed"),
11583                    new_id: ObjectId::from_hex(
11584                        ObjectFormat::Sha1,
11585                        "2222222222222222222222222222222222222222",
11586                    )
11587                    .expect("test operation should succeed"),
11588                    name: "bad ref".into(),
11589                }],
11590                ..ReceivePackRequest::default()
11591            })
11592            .is_err()
11593        );
11594    }
11595
11596    #[test]
11597    fn receive_pack_features_parse_encode_and_validate_push_request() {
11598        let capabilities = vec![
11599            Capability {
11600                name: "report-status".into(),
11601                value: None,
11602            },
11603            Capability {
11604                name: "report-status-v2".into(),
11605                value: None,
11606            },
11607            Capability {
11608                name: "delete-refs".into(),
11609                value: None,
11610            },
11611            Capability {
11612                name: "ofs-delta".into(),
11613                value: None,
11614            },
11615            Capability {
11616                name: "atomic".into(),
11617                value: None,
11618            },
11619            Capability {
11620                name: "push-options".into(),
11621                value: None,
11622            },
11623            Capability {
11624                name: "side-band-64k".into(),
11625                value: None,
11626            },
11627            Capability {
11628                name: "quiet".into(),
11629                value: None,
11630            },
11631            Capability {
11632                name: "no-thin".into(),
11633                value: None,
11634            },
11635            Capability {
11636                name: "agent".into(),
11637                value: Some("git/2.54.0".into()),
11638            },
11639            Capability {
11640                name: "object-format".into(),
11641                value: Some("sha256".into()),
11642            },
11643            Capability {
11644                name: "custom".into(),
11645                value: Some("value".into()),
11646            },
11647        ];
11648        let features =
11649            parse_receive_pack_features(&capabilities).expect("test operation should succeed");
11650        assert_eq!(
11651            features,
11652            ReceivePackFeatures {
11653                report_status: true,
11654                report_status_v2: true,
11655                delete_refs: true,
11656                ofs_delta: true,
11657                atomic: true,
11658                push_options: true,
11659                side_band_64k: true,
11660                quiet: true,
11661                no_thin: true,
11662                agent: Some("git/2.54.0".into()),
11663                object_format: Some(ObjectFormat::Sha256),
11664                unknown: vec![Capability {
11665                    name: "custom".into(),
11666                    value: Some("value".into()),
11667                }],
11668            }
11669        );
11670        assert_eq!(
11671            encode_receive_pack_features(&features).expect("test operation should succeed"),
11672            capabilities
11673        );
11674
11675        let request = ReceivePackPushRequest {
11676            commands: ReceivePackRequest {
11677                commands: vec![ReceivePackCommand {
11678                    old_id: ObjectId::from_hex(
11679                        ObjectFormat::Sha1,
11680                        "1111111111111111111111111111111111111111",
11681                    )
11682                    .expect("test operation should succeed"),
11683                    new_id: ObjectId::from_hex(
11684                        ObjectFormat::Sha1,
11685                        "2222222222222222222222222222222222222222",
11686                    )
11687                    .expect("test operation should succeed"),
11688                    name: "refs/heads/main".into(),
11689                }],
11690                capabilities: vec![
11691                    Capability {
11692                        name: "report-status".into(),
11693                        value: None,
11694                    },
11695                    Capability {
11696                        name: "ofs-delta".into(),
11697                        value: None,
11698                    },
11699                    Capability {
11700                        name: "push-options".into(),
11701                        value: None,
11702                    },
11703                    Capability {
11704                        name: "side-band-64k".into(),
11705                        value: None,
11706                    },
11707                    Capability {
11708                        name: "agent".into(),
11709                        value: Some("sley".into()),
11710                    },
11711                ],
11712                ..ReceivePackRequest::default()
11713            },
11714            push_options: Some(vec!["ci.skip".into()]),
11715            packfile: b"PACKpayload".to_vec(),
11716        };
11717        validate_receive_pack_push_request_features(&features, &request)
11718            .expect("test operation should succeed");
11719    }
11720
11721    #[test]
11722    fn receive_pack_features_reject_invalid_push_requests() {
11723        let old_id = ObjectId::from_hex(
11724            ObjectFormat::Sha1,
11725            "1111111111111111111111111111111111111111",
11726        )
11727        .expect("test operation should succeed");
11728        let new_id = ObjectId::from_hex(
11729            ObjectFormat::Sha1,
11730            "2222222222222222222222222222222222222222",
11731        )
11732        .expect("test operation should succeed");
11733        let zero = ObjectId::from_hex(
11734            ObjectFormat::Sha1,
11735            "0000000000000000000000000000000000000000",
11736        )
11737        .expect("test operation should succeed");
11738        let features = ReceivePackFeatures {
11739            report_status: true,
11740            push_options: true,
11741            ..ReceivePackFeatures::default()
11742        };
11743        let update = ReceivePackCommand {
11744            old_id: old_id.clone(),
11745            new_id: new_id.clone(),
11746            name: "refs/heads/main".into(),
11747        };
11748
11749        assert!(
11750            validate_receive_pack_push_request_features(
11751                &features,
11752                &ReceivePackPushRequest {
11753                    commands: ReceivePackRequest {
11754                        commands: vec![update.clone()],
11755                        capabilities: vec![Capability {
11756                            name: "push-options".into(),
11757                            value: None,
11758                        }],
11759                        ..ReceivePackRequest::default()
11760                    },
11761                    push_options: None,
11762                    packfile: b"PACKpayload".to_vec(),
11763                },
11764            )
11765            .is_err()
11766        );
11767        assert!(
11768            validate_receive_pack_push_request_features(
11769                &features,
11770                &ReceivePackPushRequest {
11771                    commands: ReceivePackRequest {
11772                        commands: vec![update.clone()],
11773                        ..ReceivePackRequest::default()
11774                    },
11775                    push_options: Some(Vec::new()),
11776                    packfile: b"PACKpayload".to_vec(),
11777                },
11778            )
11779            .is_err()
11780        );
11781        assert!(
11782            validate_receive_pack_push_request_features(
11783                &features,
11784                &ReceivePackPushRequest {
11785                    commands: ReceivePackRequest {
11786                        commands: vec![ReceivePackCommand {
11787                            old_id: old_id.clone(),
11788                            new_id: zero.clone(),
11789                            name: "refs/heads/main".into(),
11790                        }],
11791                        ..ReceivePackRequest::default()
11792                    },
11793                    push_options: None,
11794                    packfile: Vec::new(),
11795                },
11796            )
11797            .is_err()
11798        );
11799        validate_receive_pack_push_request_features(
11800            &features,
11801            &ReceivePackPushRequest {
11802                commands: ReceivePackRequest {
11803                    commands: vec![update.clone()],
11804                    ..ReceivePackRequest::default()
11805                },
11806                push_options: None,
11807                packfile: Vec::new(),
11808            },
11809        )
11810        .expect("updates to already-present objects may omit a packfile");
11811        assert!(
11812            validate_receive_pack_push_request_features(
11813                &ReceivePackFeatures {
11814                    delete_refs: true,
11815                    ..ReceivePackFeatures::default()
11816                },
11817                &ReceivePackPushRequest {
11818                    commands: ReceivePackRequest {
11819                        commands: vec![ReceivePackCommand {
11820                            old_id,
11821                            new_id: zero,
11822                            name: "refs/heads/main".into(),
11823                        }],
11824                        ..ReceivePackRequest::default()
11825                    },
11826                    push_options: None,
11827                    packfile: b"PACKpayload".to_vec(),
11828                },
11829            )
11830            .is_err()
11831        );
11832        assert!(
11833            validate_receive_pack_push_request_features(
11834                &features,
11835                &ReceivePackPushRequest {
11836                    commands: ReceivePackRequest {
11837                        commands: vec![update],
11838                        capabilities: vec![Capability {
11839                            name: "atomic".into(),
11840                            value: None,
11841                        }],
11842                        ..ReceivePackRequest::default()
11843                    },
11844                    push_options: None,
11845                    packfile: b"PACKpayload".to_vec(),
11846                },
11847            )
11848            .is_err()
11849        );
11850
11851        assert!(
11852            parse_receive_pack_features(&[
11853                Capability {
11854                    name: "push-options".into(),
11855                    value: None,
11856                },
11857                Capability {
11858                    name: "push-options".into(),
11859                    value: None,
11860                },
11861            ])
11862            .is_err()
11863        );
11864        assert!(
11865            encode_receive_pack_features(&ReceivePackFeatures {
11866                unknown: vec![Capability {
11867                    name: "atomic".into(),
11868                    value: None,
11869                }],
11870                ..ReceivePackFeatures::default()
11871            })
11872            .is_err()
11873        );
11874    }
11875
11876    #[test]
11877    fn receive_pack_apply_helper_installs_pack_verifies_objects_and_reports_ok() {
11878        let old_id = ObjectId::from_hex(
11879            ObjectFormat::Sha1,
11880            "1111111111111111111111111111111111111111",
11881        )
11882        .expect("test operation should succeed");
11883        let new_id = ObjectId::from_hex(
11884            ObjectFormat::Sha1,
11885            "2222222222222222222222222222222222222222",
11886        )
11887        .expect("test operation should succeed");
11888        let request = ReceivePackPushRequest {
11889            commands: ReceivePackRequest {
11890                commands: vec![ReceivePackCommand {
11891                    old_id: old_id.clone(),
11892                    new_id: new_id.clone(),
11893                    name: "refs/heads/main".into(),
11894                }],
11895                ..ReceivePackRequest::default()
11896            },
11897            packfile: b"PACKpayload".to_vec(),
11898            ..ReceivePackPushRequest::default()
11899        };
11900        let installed = std::cell::Cell::new(false);
11901        let applied = std::cell::RefCell::new(Vec::new());
11902
11903        let report = apply_receive_pack_push_request(
11904            &ReceivePackFeatures::default(),
11905            &request,
11906            |_| unreachable!("update stale-old checks belong to the ref transaction callback"),
11907            |packfile| {
11908                assert_eq!(packfile, b"PACKpayload");
11909                installed.set(true);
11910                Ok(())
11911            },
11912            |oid| Ok(oid == &new_id),
11913            |commands| {
11914                applied.borrow_mut().extend_from_slice(commands);
11915                Ok(())
11916            },
11917            |_| unreachable!("no delete command should be applied"),
11918        )
11919        .expect("test operation should succeed");
11920
11921        assert!(installed.get());
11922        assert_eq!(applied.into_inner(), request.commands.commands);
11923        assert_eq!(report.unpack, ReceivePackUnpackStatus::Ok);
11924        assert_eq!(
11925            report.commands,
11926            vec![ReceivePackCommandStatus::Ok {
11927                name: "refs/heads/main".into(),
11928            }]
11929        );
11930    }
11931
11932    #[test]
11933    fn receive_pack_apply_helper_allows_update_without_pack_when_object_exists() {
11934        let old_id = ObjectId::from_hex(
11935            ObjectFormat::Sha1,
11936            "1111111111111111111111111111111111111111",
11937        )
11938        .expect("test operation should succeed");
11939        let new_id = ObjectId::from_hex(
11940            ObjectFormat::Sha1,
11941            "2222222222222222222222222222222222222222",
11942        )
11943        .expect("test operation should succeed");
11944        let request = ReceivePackPushRequest {
11945            commands: ReceivePackRequest {
11946                commands: vec![ReceivePackCommand {
11947                    old_id: old_id.clone(),
11948                    new_id: new_id.clone(),
11949                    name: "refs/heads/main".into(),
11950                }],
11951                ..ReceivePackRequest::default()
11952            },
11953            ..ReceivePackPushRequest::default()
11954        };
11955        let installed = std::cell::Cell::new(false);
11956        let applied = std::cell::RefCell::new(Vec::new());
11957
11958        let report = apply_receive_pack_push_request(
11959            &ReceivePackFeatures::default(),
11960            &request,
11961            |_| unreachable!("update stale-old checks belong to the ref transaction callback"),
11962            |_| {
11963                installed.set(true);
11964                Ok(())
11965            },
11966            |oid| Ok(oid == &new_id),
11967            |commands| {
11968                applied.borrow_mut().extend_from_slice(commands);
11969                Ok(())
11970            },
11971            |_| unreachable!("no delete command should be applied"),
11972        )
11973        .expect("test operation should succeed");
11974
11975        assert!(!installed.get());
11976        assert_eq!(applied.into_inner(), request.commands.commands);
11977        assert_eq!(report.unpack, ReceivePackUnpackStatus::Ok);
11978    }
11979
11980    #[test]
11981    fn receive_pack_apply_helper_preserves_delete_only_and_stale_delete_rules() {
11982        let old_id = ObjectId::from_hex(
11983            ObjectFormat::Sha1,
11984            "1111111111111111111111111111111111111111",
11985        )
11986        .expect("test operation should succeed");
11987        let other_id = ObjectId::from_hex(
11988            ObjectFormat::Sha1,
11989            "2222222222222222222222222222222222222222",
11990        )
11991        .expect("test operation should succeed");
11992        let zero = zero_object_id(ObjectFormat::Sha1).expect("test operation should succeed");
11993        let request = ReceivePackPushRequest {
11994            commands: ReceivePackRequest {
11995                commands: vec![ReceivePackCommand {
11996                    old_id: old_id.clone(),
11997                    new_id: zero,
11998                    name: "refs/heads/main".into(),
11999                }],
12000                ..ReceivePackRequest::default()
12001            },
12002            ..ReceivePackPushRequest::default()
12003        };
12004        let features = ReceivePackFeatures {
12005            delete_refs: true,
12006            ..ReceivePackFeatures::default()
12007        };
12008        let installed = std::cell::Cell::new(false);
12009        let deleted = std::cell::RefCell::new(Vec::new());
12010
12011        let report = apply_receive_pack_push_request(
12012            &features,
12013            &request,
12014            |_| Ok(Some(old_id.clone())),
12015            |_| {
12016                installed.set(true);
12017                Ok(())
12018            },
12019            |_| Ok(false),
12020            |_| unreachable!("delete-only request should not apply updates"),
12021            |command| {
12022                deleted.borrow_mut().push(command.name.clone());
12023                Ok(())
12024            },
12025        )
12026        .expect("test operation should succeed");
12027
12028        assert!(!installed.get());
12029        assert_eq!(deleted.into_inner(), vec!["refs/heads/main"]);
12030        assert_eq!(report.unpack, ReceivePackUnpackStatus::Ok);
12031        assert!(
12032            apply_receive_pack_push_request(
12033                &features,
12034                &request,
12035                |_| Ok(Some(other_id.clone())),
12036                |_| Ok(()),
12037                |_| Ok(false),
12038                |_| Ok(()),
12039                |_| Ok(()),
12040            )
12041            .is_err()
12042        );
12043    }
12044
12045    #[test]
12046    fn receive_pack_push_request_parses_commands_options_and_packfile() {
12047        let command = ReceivePackCommand {
12048            old_id: ObjectId::from_hex(
12049                ObjectFormat::Sha1,
12050                "1111111111111111111111111111111111111111",
12051            )
12052            .expect("test operation should succeed"),
12053            new_id: ObjectId::from_hex(
12054                ObjectFormat::Sha1,
12055                "2222222222222222222222222222222222222222",
12056            )
12057            .expect("test operation should succeed"),
12058            name: "refs/heads/main".into(),
12059        };
12060        let expected = ReceivePackPushRequest {
12061            commands: ReceivePackRequest {
12062                commands: vec![command],
12063                capabilities: vec![
12064                    Capability {
12065                        name: "report-status".into(),
12066                        value: None,
12067                    },
12068                    Capability {
12069                        name: "push-options".into(),
12070                        value: None,
12071                    },
12072                ],
12073                ..ReceivePackRequest::default()
12074            },
12075            push_options: Some(vec!["ci.skip".into(), "deploy=staging".into()]),
12076            packfile: b"PACK\x00\x00\x00\x02payload".to_vec(),
12077        };
12078        let encoded =
12079            encode_receive_pack_push_request(&expected).expect("test operation should succeed");
12080
12081        assert_eq!(
12082            parse_receive_pack_push_request(ObjectFormat::Sha1, &encoded, true)
12083                .expect("test operation should succeed"),
12084            expected
12085        );
12086    }
12087
12088    #[test]
12089    fn receive_pack_push_request_preserves_packfile_without_push_options() {
12090        let request = ReceivePackPushRequest {
12091            commands: ReceivePackRequest {
12092                commands: vec![ReceivePackCommand {
12093                    old_id: ObjectId::from_hex(
12094                        ObjectFormat::Sha1,
12095                        "1111111111111111111111111111111111111111",
12096                    )
12097                    .expect("test operation should succeed"),
12098                    new_id: ObjectId::from_hex(
12099                        ObjectFormat::Sha1,
12100                        "2222222222222222222222222222222222222222",
12101                    )
12102                    .expect("test operation should succeed"),
12103                    name: "refs/heads/main".into(),
12104                }],
12105                ..ReceivePackRequest::default()
12106            },
12107            push_options: None,
12108            packfile: b"0000PACK-like bytes stay raw".to_vec(),
12109        };
12110        let encoded =
12111            encode_receive_pack_push_request(&request).expect("test operation should succeed");
12112
12113        assert_eq!(
12114            parse_receive_pack_push_request(ObjectFormat::Sha1, &encoded, false)
12115                .expect("test operation should succeed"),
12116            request
12117        );
12118    }
12119
12120    #[test]
12121    fn receive_pack_push_request_streams_round_trip() {
12122        let request = ReceivePackPushRequest {
12123            commands: ReceivePackRequest {
12124                commands: vec![ReceivePackCommand {
12125                    old_id: ObjectId::from_hex(
12126                        ObjectFormat::Sha1,
12127                        "1111111111111111111111111111111111111111",
12128                    )
12129                    .expect("test operation should succeed"),
12130                    new_id: ObjectId::from_hex(
12131                        ObjectFormat::Sha1,
12132                        "2222222222222222222222222222222222222222",
12133                    )
12134                    .expect("test operation should succeed"),
12135                    name: "refs/heads/main".into(),
12136                }],
12137                capabilities: vec![Capability {
12138                    name: "push-options".into(),
12139                    value: None,
12140                }],
12141                ..ReceivePackRequest::default()
12142            },
12143            push_options: Some(Vec::new()),
12144            packfile: b"PACKpayload".to_vec(),
12145        };
12146        let mut encoded = Vec::new();
12147        write_receive_pack_push_request(&mut encoded, &request)
12148            .expect("test operation should succeed");
12149
12150        assert_eq!(
12151            read_receive_pack_push_request(ObjectFormat::Sha1, &mut encoded.as_slice(), true)
12152                .expect("test operation should succeed"),
12153            request
12154        );
12155    }
12156
12157    #[test]
12158    fn receive_pack_push_request_rejects_malformed_sections() {
12159        assert!(
12160            parse_receive_pack_push_request(
12161                ObjectFormat::Sha1,
12162                b"0014not-a-command\n0000PACK",
12163                false,
12164            )
12165            .is_err()
12166        );
12167
12168        let request = ReceivePackPushRequest {
12169            commands: ReceivePackRequest {
12170                commands: vec![ReceivePackCommand {
12171                    old_id: ObjectId::from_hex(
12172                        ObjectFormat::Sha1,
12173                        "1111111111111111111111111111111111111111",
12174                    )
12175                    .expect("test operation should succeed"),
12176                    new_id: ObjectId::from_hex(
12177                        ObjectFormat::Sha1,
12178                        "2222222222222222222222222222222222222222",
12179                    )
12180                    .expect("test operation should succeed"),
12181                    name: "refs/heads/main".into(),
12182                }],
12183                ..ReceivePackRequest::default()
12184            },
12185            push_options: None,
12186            packfile: b"PACKpayload".to_vec(),
12187        };
12188        let encoded =
12189            encode_receive_pack_push_request(&request).expect("test operation should succeed");
12190        assert!(parse_receive_pack_push_request(ObjectFormat::Sha1, &encoded, true).is_err());
12191
12192        assert!(
12193            encode_receive_pack_push_request(&ReceivePackPushRequest {
12194                commands: ReceivePackRequest {
12195                    shallow: vec![
12196                        ObjectId::from_hex(
12197                            ObjectFormat::Sha1,
12198                            "1111111111111111111111111111111111111111",
12199                        )
12200                        .expect("test operation should succeed")
12201                    ],
12202                    ..ReceivePackRequest::default()
12203                },
12204                push_options: None,
12205                packfile: Vec::new(),
12206            })
12207            .is_err()
12208        );
12209    }
12210
12211    #[test]
12212    fn receive_pack_report_status_parses_and_encodes_status_lines() {
12213        let frames = vec![
12214            PktLineFrame::Data(b"unpack ok\n".to_vec()),
12215            PktLineFrame::Data(b"ok refs/heads/main\n".to_vec()),
12216            PktLineFrame::Data(b"ng refs/heads/old non-fast-forward\n".to_vec()),
12217            PktLineFrame::Flush,
12218        ];
12219        let report =
12220            parse_receive_pack_report_status(&frames).expect("test operation should succeed");
12221        assert_eq!(
12222            report,
12223            ReceivePackReportStatus {
12224                unpack: ReceivePackUnpackStatus::Ok,
12225                commands: vec![
12226                    ReceivePackCommandStatus::Ok {
12227                        name: "refs/heads/main".into(),
12228                    },
12229                    ReceivePackCommandStatus::Ng {
12230                        name: "refs/heads/old".into(),
12231                        message: "non-fast-forward".into(),
12232                    },
12233                ],
12234            }
12235        );
12236        assert_eq!(
12237            encode_receive_pack_report_status(&report).expect("test operation should succeed"),
12238            frames
12239        );
12240
12241        let frames = vec![
12242            PktLineFrame::Data(b"unpack pack exceeds maximum size\n".to_vec()),
12243            PktLineFrame::Flush,
12244        ];
12245        assert_eq!(
12246            parse_receive_pack_report_status(&frames).expect("test operation should succeed"),
12247            ReceivePackReportStatus {
12248                unpack: ReceivePackUnpackStatus::Error("pack exceeds maximum size".into()),
12249                commands: Vec::new(),
12250            }
12251        );
12252    }
12253
12254    #[test]
12255    fn receive_pack_report_status_streams_round_trip() {
12256        let report = ReceivePackReportStatus {
12257            unpack: ReceivePackUnpackStatus::Ok,
12258            commands: vec![ReceivePackCommandStatus::Ok {
12259                name: "refs/heads/main".into(),
12260            }],
12261        };
12262        let mut encoded = Vec::new();
12263        write_receive_pack_report_status(&mut encoded, &report)
12264            .expect("test operation should succeed");
12265        encoded.extend_from_slice(b"tail");
12266
12267        let mut input = encoded.as_slice();
12268        assert_eq!(
12269            read_receive_pack_report_status(&mut input).expect("test operation should succeed"),
12270            report
12271        );
12272        assert_eq!(input, b"tail");
12273    }
12274
12275    #[test]
12276    fn receive_pack_report_status_rejects_malformed_status_lines() {
12277        assert!(parse_receive_pack_report_status(&[]).is_err());
12278        assert!(
12279            parse_receive_pack_report_status(&[
12280                PktLineFrame::Data(b"unpack ok\n".to_vec()),
12281                PktLineFrame::Data(b"ok refs/heads/main\n".to_vec()),
12282            ])
12283            .is_err()
12284        );
12285        assert!(
12286            parse_receive_pack_report_status(&[
12287                PktLineFrame::Flush,
12288                PktLineFrame::Data(b"ok refs/heads/main\n".to_vec()),
12289            ])
12290            .is_err()
12291        );
12292        assert!(
12293            parse_receive_pack_report_status(&[
12294                PktLineFrame::Data(b"unpack ok\n".to_vec()),
12295                PktLineFrame::Data(b"bad refs/heads/main\n".to_vec()),
12296                PktLineFrame::Flush,
12297            ])
12298            .is_err()
12299        );
12300        assert!(
12301            parse_receive_pack_report_status(&[
12302                PktLineFrame::Data(b"unpack ok\n".to_vec()),
12303                PktLineFrame::Data(b"ng refs/heads/main\n".to_vec()),
12304                PktLineFrame::Flush,
12305            ])
12306            .is_err()
12307        );
12308        assert!(
12309            encode_receive_pack_report_status(&ReceivePackReportStatus {
12310                unpack: ReceivePackUnpackStatus::Error("".into()),
12311                commands: Vec::new(),
12312            })
12313            .is_err()
12314        );
12315        assert!(
12316            encode_receive_pack_report_status(&ReceivePackReportStatus {
12317                unpack: ReceivePackUnpackStatus::Ok,
12318                commands: vec![ReceivePackCommandStatus::Ok {
12319                    name: "bad ref".into(),
12320                }],
12321            })
12322            .is_err()
12323        );
12324    }
12325
12326    #[test]
12327    fn receive_pack_report_status_v2_parses_and_encodes_options() {
12328        let old_oid = ObjectId::from_hex(
12329            ObjectFormat::Sha1,
12330            "1111111111111111111111111111111111111111",
12331        )
12332        .expect("test operation should succeed");
12333        let new_oid = ObjectId::from_hex(
12334            ObjectFormat::Sha1,
12335            "2222222222222222222222222222222222222222",
12336        )
12337        .expect("test operation should succeed");
12338        let frames = vec![
12339            PktLineFrame::Data(b"unpack ok\n".to_vec()),
12340            PktLineFrame::Data(b"ok refs/for/main\n".to_vec()),
12341            PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12342            PktLineFrame::Data(
12343                b"option old-oid 1111111111111111111111111111111111111111\n".to_vec(),
12344            ),
12345            PktLineFrame::Data(
12346                b"option new-oid 2222222222222222222222222222222222222222\n".to_vec(),
12347            ),
12348            PktLineFrame::Data(b"option forced-update\n".to_vec()),
12349            PktLineFrame::Data(b"ng refs/heads/old rejected by hook\n".to_vec()),
12350            PktLineFrame::Flush,
12351        ];
12352        let report = parse_receive_pack_report_status_v2(ObjectFormat::Sha1, &frames)
12353            .expect("test operation should succeed");
12354        assert_eq!(
12355            report,
12356            ReceivePackReportStatusV2 {
12357                unpack: ReceivePackUnpackStatus::Ok,
12358                commands: vec![
12359                    ReceivePackCommandStatusV2::Ok {
12360                        name: "refs/for/main".into(),
12361                        options: ReceivePackCommandStatusV2Options {
12362                            refname: Some("refs/heads/main".into()),
12363                            old_oid: Some(old_oid),
12364                            new_oid: Some(new_oid),
12365                            forced_update: true,
12366                        },
12367                    },
12368                    ReceivePackCommandStatusV2::Ng {
12369                        name: "refs/heads/old".into(),
12370                        message: "rejected by hook".into(),
12371                    },
12372                ],
12373            }
12374        );
12375        assert_eq!(
12376            encode_receive_pack_report_status_v2(&report).expect("test operation should succeed"),
12377            frames
12378        );
12379    }
12380
12381    #[test]
12382    fn receive_pack_report_status_v2_streams_round_trip() {
12383        let report = ReceivePackReportStatusV2 {
12384            unpack: ReceivePackUnpackStatus::Ok,
12385            commands: vec![ReceivePackCommandStatusV2::Ok {
12386                name: "refs/for/main".into(),
12387                options: ReceivePackCommandStatusV2Options {
12388                    refname: Some("refs/heads/main".into()),
12389                    old_oid: None,
12390                    new_oid: None,
12391                    forced_update: false,
12392                },
12393            }],
12394        };
12395        let mut encoded = Vec::new();
12396        write_receive_pack_report_status_v2(&mut encoded, &report)
12397            .expect("test operation should succeed");
12398        encoded.extend_from_slice(b"tail");
12399
12400        let mut input = encoded.as_slice();
12401        assert_eq!(
12402            read_receive_pack_report_status_v2(ObjectFormat::Sha1, &mut input)
12403                .expect("test operation should succeed"),
12404            report
12405        );
12406        assert_eq!(input, b"tail");
12407    }
12408
12409    #[test]
12410    fn receive_pack_report_status_v2_rejects_malformed_options() {
12411        assert!(parse_receive_pack_report_status_v2(ObjectFormat::Sha1, &[]).is_err());
12412        assert!(
12413            parse_receive_pack_report_status_v2(
12414                ObjectFormat::Sha1,
12415                &[
12416                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12417                    PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12418                    PktLineFrame::Flush,
12419                ],
12420            )
12421            .is_err()
12422        );
12423        assert!(
12424            parse_receive_pack_report_status_v2(
12425                ObjectFormat::Sha1,
12426                &[
12427                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12428                    PktLineFrame::Data(b"ng refs/heads/main rejected\n".to_vec()),
12429                    PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12430                    PktLineFrame::Flush,
12431                ],
12432            )
12433            .is_err()
12434        );
12435        assert!(
12436            parse_receive_pack_report_status_v2(
12437                ObjectFormat::Sha1,
12438                &[
12439                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12440                    PktLineFrame::Data(b"ok refs/for/main\n".to_vec()),
12441                    PktLineFrame::Data(b"option refname refs/heads/main\n".to_vec()),
12442                    PktLineFrame::Data(b"option refname refs/heads/next\n".to_vec()),
12443                    PktLineFrame::Flush,
12444                ],
12445            )
12446            .is_err()
12447        );
12448        assert!(
12449            parse_receive_pack_report_status_v2(
12450                ObjectFormat::Sha1,
12451                &[
12452                    PktLineFrame::Data(b"unpack ok\n".to_vec()),
12453                    PktLineFrame::Data(b"ok refs/for/main\n".to_vec()),
12454                    PktLineFrame::Data(b"option old-oid not-an-oid\n".to_vec()),
12455                    PktLineFrame::Flush,
12456                ],
12457            )
12458            .is_err()
12459        );
12460        assert!(
12461            encode_receive_pack_report_status_v2(&ReceivePackReportStatusV2 {
12462                unpack: ReceivePackUnpackStatus::Ok,
12463                commands: vec![ReceivePackCommandStatusV2::Ok {
12464                    name: "refs/for/main".into(),
12465                    options: ReceivePackCommandStatusV2Options {
12466                        refname: Some("bad ref".into()),
12467                        ..ReceivePackCommandStatusV2Options::default()
12468                    },
12469                }],
12470            })
12471            .is_err()
12472        );
12473    }
12474
12475    #[test]
12476    fn receive_pack_push_options_parse_and_encode_options() {
12477        let frames = vec![
12478            PktLineFrame::Data(b"ci.skip\n".to_vec()),
12479            PktLineFrame::Data(b"deploy target=staging\n".to_vec()),
12480            PktLineFrame::Data(b"\n".to_vec()),
12481            PktLineFrame::Flush,
12482        ];
12483        let options =
12484            parse_receive_pack_push_options(&frames).expect("test operation should succeed");
12485        assert_eq!(
12486            options,
12487            vec![
12488                "ci.skip".to_string(),
12489                "deploy target=staging".to_string(),
12490                String::new(),
12491            ]
12492        );
12493        assert_eq!(
12494            encode_receive_pack_push_options(&options).expect("test operation should succeed"),
12495            frames
12496        );
12497        assert_eq!(
12498            parse_receive_pack_push_options(&[PktLineFrame::Flush])
12499                .expect("test operation should succeed"),
12500            Vec::<String>::new()
12501        );
12502    }
12503
12504    #[test]
12505    fn receive_pack_push_options_streams_round_trip() {
12506        let options = vec!["ci.skip".to_string(), "reviewer=alice".to_string()];
12507        let mut encoded = Vec::new();
12508        write_receive_pack_push_options(&mut encoded, &options)
12509            .expect("test operation should succeed");
12510        encoded.extend_from_slice(b"PACK");
12511
12512        let mut input = encoded.as_slice();
12513        assert_eq!(
12514            read_receive_pack_push_options(&mut input).expect("test operation should succeed"),
12515            options
12516        );
12517        assert_eq!(input, b"PACK");
12518    }
12519
12520    #[test]
12521    fn receive_pack_push_options_reject_malformed_streams() {
12522        assert!(
12523            parse_receive_pack_push_options(&[PktLineFrame::Data(b"ci.skip\n".to_vec())]).is_err()
12524        );
12525        assert!(
12526            parse_receive_pack_push_options(&[PktLineFrame::Delimiter, PktLineFrame::Flush])
12527                .is_err()
12528        );
12529        assert!(
12530            parse_receive_pack_push_options(&[
12531                PktLineFrame::Data(b"ci.skip\n".to_vec()),
12532                PktLineFrame::Flush,
12533                PktLineFrame::Data(b"after\n".to_vec()),
12534            ])
12535            .is_err()
12536        );
12537        assert!(
12538            parse_receive_pack_push_options(&[
12539                PktLineFrame::Data(b"bad\0option\n".to_vec()),
12540                PktLineFrame::Flush,
12541            ])
12542            .is_err()
12543        );
12544        assert!(encode_receive_pack_push_options(&["bad\noption".to_string()]).is_err());
12545    }
12546
12547    #[test]
12548    fn protocol_v2_advertisement_parses_version_and_capabilities() {
12549        let frames = parse_pkt_line_stream(
12550            b"000eversion 2\n0015agent=git/2.54.0\n0013ls-refs=unborn\n0027fetch=shallow wait-for-done filter\n0012server-option\n0000",
12551        )
12552        .expect("test operation should succeed");
12553        let handshake =
12554            parse_protocol_v2_advertisement(&frames).expect("test operation should succeed");
12555        assert_eq!(handshake.protocol, ProtocolVersion::V2);
12556        assert_eq!(
12557            handshake.capabilities,
12558            vec![
12559                Capability {
12560                    name: "agent".into(),
12561                    value: Some("git/2.54.0".into()),
12562                },
12563                Capability {
12564                    name: "ls-refs".into(),
12565                    value: Some("unborn".into()),
12566                },
12567                Capability {
12568                    name: "fetch".into(),
12569                    value: Some("shallow wait-for-done filter".into()),
12570                },
12571                Capability {
12572                    name: "server-option".into(),
12573                    value: None,
12574                },
12575            ]
12576        );
12577        assert_eq!(
12578            encode_protocol_v2_advertisement(&handshake).expect("test operation should succeed"),
12579            frames
12580        );
12581    }
12582
12583    #[test]
12584    fn protocol_v2_advertisement_reads_until_flush() {
12585        let mut input = b"000eversion 2\n0013ls-refs=unborn\n0000next-session".as_slice();
12586        let handshake =
12587            read_protocol_v2_advertisement(&mut input).expect("test operation should succeed");
12588        assert_eq!(handshake.protocol, ProtocolVersion::V2);
12589        assert_eq!(
12590            handshake.capabilities,
12591            vec![Capability {
12592                name: "ls-refs".into(),
12593                value: Some("unborn".into()),
12594            }]
12595        );
12596        assert_eq!(input, b"next-session");
12597    }
12598
12599    #[test]
12600    fn protocol_v2_advertisement_writes_stream() {
12601        let handshake = TransportHandshake {
12602            protocol: ProtocolVersion::V2,
12603            capabilities: vec![
12604                Capability {
12605                    name: "agent".into(),
12606                    value: Some("sley/0".into()),
12607                },
12608                Capability {
12609                    name: "fetch".into(),
12610                    value: Some("shallow filter".into()),
12611                },
12612            ],
12613        };
12614        let mut encoded = Vec::new();
12615        write_protocol_v2_advertisement(&mut encoded, &handshake)
12616            .expect("test operation should succeed");
12617        let mut input = encoded.as_slice();
12618        assert_eq!(
12619            read_protocol_v2_advertisement(&mut input).expect("test operation should succeed"),
12620            handshake
12621        );
12622        assert!(input.is_empty());
12623        assert!(
12624            encode_protocol_v2_advertisement(&TransportHandshake {
12625                protocol: ProtocolVersion::V1,
12626                capabilities: Vec::new(),
12627            })
12628            .is_err()
12629        );
12630    }
12631
12632    #[test]
12633    fn protocol_v2_advertisement_rejects_malformed_sequences() {
12634        assert!(parse_protocol_v2_advertisement(&[]).is_err());
12635        assert!(
12636            parse_protocol_v2_advertisement(&[
12637                PktLineFrame::Data(b"version 1\n".to_vec()),
12638                PktLineFrame::Flush,
12639            ])
12640            .is_err()
12641        );
12642        assert!(
12643            parse_protocol_v2_advertisement(&[PktLineFrame::Data(b"version 2\n".to_vec())])
12644                .is_err()
12645        );
12646        assert!(
12647            parse_protocol_v2_advertisement(&[
12648                PktLineFrame::Data(b"version 2\n".to_vec()),
12649                PktLineFrame::Delimiter,
12650            ])
12651            .is_err()
12652        );
12653        assert!(
12654            parse_protocol_v2_advertisement(&[
12655                PktLineFrame::Data(b"version 2\n".to_vec()),
12656                PktLineFrame::Data(b"fetch=\n".to_vec()),
12657                PktLineFrame::Flush,
12658            ])
12659            .is_err()
12660        );
12661    }
12662
12663    #[test]
12664    fn protocol_v2_command_request_parses_and_encodes_sections() {
12665        let frames = parse_pkt_line_stream(
12666            b"0014command=ls-refs\n0011agent=sley/0\n0017object-format=sha1\n00010009peel\n000csymrefs\n001bref-prefix refs/heads/\n0000",
12667        )
12668        .expect("test operation should succeed");
12669        let request =
12670            parse_protocol_v2_command_request(&frames).expect("test operation should succeed");
12671        assert_eq!(
12672            request,
12673            ProtocolV2CommandRequest {
12674                command: "ls-refs".into(),
12675                capabilities: vec![
12676                    Capability {
12677                        name: "agent".into(),
12678                        value: Some("sley/0".into()),
12679                    },
12680                    Capability {
12681                        name: "object-format".into(),
12682                        value: Some("sha1".into()),
12683                    },
12684                ],
12685                arguments: vec![
12686                    b"peel".to_vec(),
12687                    b"symrefs".to_vec(),
12688                    b"ref-prefix refs/heads/".to_vec(),
12689                ],
12690            }
12691        );
12692        assert_eq!(
12693            encode_protocol_v2_command_request(&request).expect("test operation should succeed"),
12694            frames
12695        );
12696    }
12697
12698    #[test]
12699    fn protocol_v2_command_request_allows_no_argument_section() {
12700        let frames = parse_pkt_line_stream(b"0012command=fetch\n0000")
12701            .expect("test operation should succeed");
12702        let request =
12703            parse_protocol_v2_command_request(&frames).expect("test operation should succeed");
12704        assert_eq!(
12705            request,
12706            ProtocolV2CommandRequest {
12707                command: "fetch".into(),
12708                capabilities: Vec::new(),
12709                arguments: Vec::new(),
12710            }
12711        );
12712        assert_eq!(
12713            encode_protocol_v2_command_request(&request).expect("test operation should succeed"),
12714            frames
12715        );
12716    }
12717
12718    #[test]
12719    fn protocol_v2_request_parses_commands_and_empty_done() {
12720        let frames = parse_pkt_line_stream(b"0012command=fetch\n0000")
12721            .expect("test operation should succeed");
12722        let command = ProtocolV2CommandRequest {
12723            command: "fetch".into(),
12724            capabilities: Vec::new(),
12725            arguments: Vec::new(),
12726        };
12727        assert_eq!(
12728            parse_protocol_v2_request(&frames).expect("test operation should succeed"),
12729            ProtocolV2Request::Command(command.clone())
12730        );
12731        assert_eq!(
12732            encode_protocol_v2_request(&ProtocolV2Request::Command(command))
12733                .expect("test operation should succeed"),
12734            frames
12735        );
12736
12737        assert_eq!(
12738            parse_protocol_v2_request(&[PktLineFrame::Flush])
12739                .expect("test operation should succeed"),
12740            ProtocolV2Request::Done
12741        );
12742        assert_eq!(
12743            encode_protocol_v2_request(&ProtocolV2Request::Done)
12744                .expect("test operation should succeed"),
12745            vec![PktLineFrame::Flush]
12746        );
12747    }
12748
12749    #[test]
12750    fn protocol_v2_request_streams_empty_done() {
12751        let mut encoded = Vec::new();
12752        write_protocol_v2_request(&mut encoded, &ProtocolV2Request::Done)
12753            .expect("test operation should succeed");
12754        encoded.extend_from_slice(b"tail");
12755
12756        let mut input = encoded.as_slice();
12757        assert_eq!(
12758            read_protocol_v2_request(&mut input).expect("test operation should succeed"),
12759            ProtocolV2Request::Done
12760        );
12761        assert_eq!(input, b"tail");
12762        let mut command_input = encoded.as_slice();
12763        assert!(read_protocol_v2_command_request(&mut command_input).is_err());
12764    }
12765
12766    #[test]
12767    fn protocol_v2_command_request_streams_round_trip() {
12768        let request = ProtocolV2CommandRequest {
12769            command: "ls-refs".into(),
12770            capabilities: vec![Capability {
12771                name: "agent".into(),
12772                value: Some("sley/0".into()),
12773            }],
12774            arguments: vec![b"peel".to_vec(), b"symrefs".to_vec()],
12775        };
12776        let mut encoded = Vec::new();
12777        write_protocol_v2_command_request(&mut encoded, &request)
12778            .expect("test operation should succeed");
12779        encoded.extend_from_slice(b"tail");
12780
12781        let mut input = encoded.as_slice();
12782        assert_eq!(
12783            read_protocol_v2_command_request(&mut input).expect("test operation should succeed"),
12784            request
12785        );
12786        assert_eq!(input, b"tail");
12787    }
12788
12789    #[test]
12790    fn protocol_v2_command_request_rejects_malformed_sequences() {
12791        assert!(parse_protocol_v2_command_request(&[]).is_err());
12792        assert!(
12793            parse_protocol_v2_command_request(&[
12794                PktLineFrame::Data(b"agent=sley/0\n".to_vec()),
12795                PktLineFrame::Flush,
12796            ])
12797            .is_err()
12798        );
12799        assert!(
12800            parse_protocol_v2_command_request(&[
12801                PktLineFrame::Data(b"command=ls-refs\n".to_vec()),
12802                PktLineFrame::Delimiter,
12803                PktLineFrame::Delimiter,
12804                PktLineFrame::Flush,
12805            ])
12806            .is_err()
12807        );
12808        assert!(
12809            parse_protocol_v2_command_request(&[
12810                PktLineFrame::Data(b"command=ls-refs\n".to_vec()),
12811                PktLineFrame::Delimiter,
12812                PktLineFrame::Data(b"\n".to_vec()),
12813                PktLineFrame::Flush,
12814            ])
12815            .is_err()
12816        );
12817        assert!(
12818            encode_protocol_v2_command_request(&ProtocolV2CommandRequest {
12819                command: "bad command".into(),
12820                capabilities: Vec::new(),
12821                arguments: Vec::new(),
12822            })
12823            .is_err()
12824        );
12825    }
12826
12827    #[test]
12828    fn protocol_v2_ls_refs_request_parses_and_encodes_arguments() {
12829        let command = ProtocolV2CommandRequest {
12830            command: "ls-refs".into(),
12831            capabilities: Vec::new(),
12832            arguments: vec![
12833                b"peel".to_vec(),
12834                b"symrefs".to_vec(),
12835                b"unborn".to_vec(),
12836                b"ref-prefix HEAD".to_vec(),
12837                b"ref-prefix refs/heads/".to_vec(),
12838            ],
12839        };
12840        let request = ProtocolV2LsRefsRequest::from_command_request(&command)
12841            .expect("test operation should succeed");
12842        assert_eq!(
12843            request,
12844            ProtocolV2LsRefsRequest {
12845                peel: true,
12846                symrefs: true,
12847                unborn: true,
12848                ref_prefixes: vec!["HEAD".into(), "refs/heads/".into()],
12849            }
12850        );
12851        assert_eq!(
12852            request
12853                .to_command_request()
12854                .expect("test operation should succeed"),
12855            command
12856        );
12857        assert!(
12858            ProtocolV2LsRefsRequest::from_command_request(&ProtocolV2CommandRequest {
12859                command: "fetch".into(),
12860                capabilities: Vec::new(),
12861                arguments: Vec::new(),
12862            })
12863            .is_err()
12864        );
12865        assert!(
12866            ProtocolV2LsRefsRequest::from_command_request(&ProtocolV2CommandRequest {
12867                command: "ls-refs".into(),
12868                capabilities: Vec::new(),
12869                arguments: vec![b"ref-prefix ".to_vec()],
12870            })
12871            .is_err()
12872        );
12873    }
12874
12875    #[test]
12876    fn protocol_v2_ls_refs_request_streams_round_trip() {
12877        let request = ProtocolV2LsRefsRequest {
12878            peel: true,
12879            symrefs: true,
12880            unborn: false,
12881            ref_prefixes: vec!["HEAD".into(), "refs/tags/".into()],
12882        };
12883        let mut encoded = Vec::new();
12884        write_protocol_v2_ls_refs_request(&mut encoded, &request)
12885            .expect("test operation should succeed");
12886        encoded.extend_from_slice(b"tail");
12887
12888        let mut input = encoded.as_slice();
12889        assert_eq!(
12890            read_protocol_v2_ls_refs_request(&mut input).expect("test operation should succeed"),
12891            request
12892        );
12893        assert_eq!(input, b"tail");
12894    }
12895
12896    #[test]
12897    fn protocol_v2_ls_refs_response_parses_and_encodes_records() {
12898        let oid = ObjectId::from_hex(
12899            ObjectFormat::Sha1,
12900            "1111111111111111111111111111111111111111",
12901        )
12902        .expect("test operation should succeed");
12903        let peeled = ObjectId::from_hex(
12904            ObjectFormat::Sha1,
12905            "2222222222222222222222222222222222222222",
12906        )
12907        .expect("test operation should succeed");
12908        let frames = vec![
12909            PktLineFrame::Data(
12910                b"1111111111111111111111111111111111111111 refs/tags/v1 peeled:2222222222222222222222222222222222222222 symref-target:refs/heads/main custom\n"
12911                    .to_vec(),
12912            ),
12913            PktLineFrame::Data(b"unborn HEAD symref-target:refs/heads/main\n".to_vec()),
12914            PktLineFrame::Flush,
12915        ];
12916        let records = parse_protocol_v2_ls_refs_response(ObjectFormat::Sha1, &frames)
12917            .expect("test operation should succeed");
12918        assert_eq!(
12919            records,
12920            vec![
12921                ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
12922                    oid,
12923                    name: "refs/tags/v1".into(),
12924                    peeled: Some(peeled),
12925                    symref_target: Some("refs/heads/main".into()),
12926                    attributes: vec!["custom".into()],
12927                }),
12928                ProtocolV2LsRefsRecord::Unborn {
12929                    name: "HEAD".into(),
12930                    symref_target: Some("refs/heads/main".into()),
12931                    attributes: Vec::new(),
12932                },
12933            ]
12934        );
12935        assert_eq!(
12936            encode_protocol_v2_ls_refs_response(&records).expect("test operation should succeed"),
12937            frames
12938        );
12939    }
12940
12941    #[test]
12942    fn protocol_v2_ls_refs_response_streams_round_trip() {
12943        let oid = ObjectId::from_hex(
12944            ObjectFormat::Sha1,
12945            "1111111111111111111111111111111111111111",
12946        )
12947        .expect("test operation should succeed");
12948        let records = vec![ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
12949            oid,
12950            name: "refs/heads/main".into(),
12951            peeled: None,
12952            symref_target: Some("refs/heads/trunk".into()),
12953            attributes: vec!["custom".into()],
12954        })];
12955        let mut encoded = Vec::new();
12956        write_protocol_v2_ls_refs_response(&mut encoded, &records)
12957            .expect("test operation should succeed");
12958        encoded.extend_from_slice(b"tail");
12959
12960        let mut input = encoded.as_slice();
12961        assert_eq!(
12962            read_protocol_v2_ls_refs_response(ObjectFormat::Sha1, &mut input)
12963                .expect("test operation should succeed"),
12964            records
12965        );
12966        assert_eq!(input, b"tail");
12967    }
12968
12969    #[test]
12970    fn protocol_v2_ls_refs_response_reads_stateless_response_end() {
12971        let oid = ObjectId::from_hex(
12972            ObjectFormat::Sha1,
12973            "1111111111111111111111111111111111111111",
12974        )
12975        .expect("test operation should succeed");
12976        let records = vec![ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
12977            oid,
12978            name: "refs/heads/main".into(),
12979            peeled: None,
12980            symref_target: None,
12981            attributes: Vec::new(),
12982        })];
12983        let mut encoded = Vec::new();
12984        write_protocol_v2_ls_refs_response_with_response_end(&mut encoded, &records)
12985            .expect("test operation should succeed");
12986        encoded.extend_from_slice(b"tail");
12987
12988        let mut input = encoded.as_slice();
12989        assert_eq!(
12990            read_protocol_v2_ls_refs_response_until_response_end(ObjectFormat::Sha1, &mut input)
12991                .expect("test operation should succeed"),
12992            records
12993        );
12994        assert_eq!(input, b"tail");
12995        assert!(
12996            parse_protocol_v2_ls_refs_response(
12997                ObjectFormat::Sha1,
12998                &[
12999                    PktLineFrame::Data(
13000                        b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec()
13001                    ),
13002                    PktLineFrame::ResponseEnd
13003                ],
13004            )
13005            .is_err()
13006        );
13007    }
13008
13009    #[test]
13010    fn protocol_v2_ls_refs_exchange_writes_request_and_reads_response() {
13011        let oid = ObjectId::from_hex(
13012            ObjectFormat::Sha1,
13013            "1111111111111111111111111111111111111111",
13014        )
13015        .expect("test operation should succeed");
13016        let request = ProtocolV2LsRefsRequest {
13017            peel: true,
13018            symrefs: true,
13019            unborn: false,
13020            ref_prefixes: vec!["refs/heads/".into()],
13021        };
13022        let records = vec![ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
13023            oid,
13024            name: "refs/heads/main".into(),
13025            peeled: None,
13026            symref_target: None,
13027            attributes: Vec::new(),
13028        })];
13029        let mut response = Vec::new();
13030        write_protocol_v2_ls_refs_response(&mut response, &records)
13031            .expect("test operation should succeed");
13032
13033        let mut input = response.as_slice();
13034        let mut output = Vec::new();
13035        assert_eq!(
13036            exchange_protocol_v2_ls_refs(ObjectFormat::Sha1, &mut input, &mut output, &request)
13037                .expect("test operation should succeed"),
13038            records
13039        );
13040        assert!(input.is_empty());
13041        let mut output_read = output.as_slice();
13042        assert_eq!(
13043            read_protocol_v2_ls_refs_request(&mut output_read)
13044                .expect("test operation should succeed"),
13045            request
13046        );
13047    }
13048
13049    #[test]
13050    fn protocol_v2_ls_refs_response_rejects_malformed_records() {
13051        assert!(
13052            parse_protocol_v2_ls_refs_response(
13053                ObjectFormat::Sha1,
13054                &[PktLineFrame::Data(
13055                    b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec()
13056                )],
13057            )
13058            .is_err()
13059        );
13060        assert!(
13061            parse_protocol_v2_ls_refs_response(
13062                ObjectFormat::Sha1,
13063                &[
13064                    PktLineFrame::Data(
13065                        b"1111111111111111111111111111111111111111 refs/heads/main peeled:2222222222222222222222222222222222222222 peeled:3333333333333333333333333333333333333333\n"
13066                            .to_vec()
13067                    ),
13068                    PktLineFrame::Flush,
13069                ],
13070            )
13071            .is_err()
13072        );
13073        assert!(
13074            parse_protocol_v2_ls_refs_response(
13075                ObjectFormat::Sha1,
13076                &[
13077                    PktLineFrame::Data(
13078                        b"unborn HEAD peeled:2222222222222222222222222222222222222222\n".to_vec()
13079                    ),
13080                    PktLineFrame::Flush,
13081                ],
13082            )
13083            .is_err()
13084        );
13085        assert!(
13086            encode_protocol_v2_ls_refs_response(&[ProtocolV2LsRefsRecord::Ref(
13087                ProtocolV2LsRefsRef {
13088                    oid: ObjectId::from_hex(
13089                        ObjectFormat::Sha1,
13090                        "1111111111111111111111111111111111111111",
13091                    )
13092                    .expect("test operation should succeed"),
13093                    name: "refs/heads/main".into(),
13094                    peeled: None,
13095                    symref_target: None,
13096                    attributes: vec!["peeled:2222222222222222222222222222222222222222".into()],
13097                }
13098            )])
13099            .is_err()
13100        );
13101    }
13102
13103    #[test]
13104    fn protocol_v2_fetch_request_parses_and_encodes_arguments() {
13105        let want = ObjectId::from_hex(
13106            ObjectFormat::Sha1,
13107            "1111111111111111111111111111111111111111",
13108        )
13109        .expect("test operation should succeed");
13110        let have = ObjectId::from_hex(
13111            ObjectFormat::Sha1,
13112            "2222222222222222222222222222222222222222",
13113        )
13114        .expect("test operation should succeed");
13115        let shallow = ObjectId::from_hex(
13116            ObjectFormat::Sha1,
13117            "3333333333333333333333333333333333333333",
13118        )
13119        .expect("test operation should succeed");
13120        let command = ProtocolV2CommandRequest {
13121            command: "fetch".into(),
13122            capabilities: Vec::new(),
13123            arguments: vec![
13124                b"want 1111111111111111111111111111111111111111".to_vec(),
13125                b"want-ref refs/heads/main".to_vec(),
13126                b"have 2222222222222222222222222222222222222222".to_vec(),
13127                b"shallow 3333333333333333333333333333333333333333".to_vec(),
13128                b"deepen 10".to_vec(),
13129                b"deepen-since 123456789".to_vec(),
13130                b"deepen-not refs/tags/v1".to_vec(),
13131                b"deepen-relative".to_vec(),
13132                b"filter blob:none".to_vec(),
13133                b"packfile-uris http,https".to_vec(),
13134                b"thin-pack".to_vec(),
13135                b"no-progress".to_vec(),
13136                b"include-tag".to_vec(),
13137                b"ofs-delta".to_vec(),
13138                b"sideband-all".to_vec(),
13139                b"wait-for-done".to_vec(),
13140                b"done".to_vec(),
13141            ],
13142        };
13143        let request = ProtocolV2FetchRequest::from_command_request(ObjectFormat::Sha1, &command)
13144            .expect("test operation should succeed");
13145        assert_eq!(
13146            request,
13147            ProtocolV2FetchRequest {
13148                wants: vec![want],
13149                want_refs: vec!["refs/heads/main".into()],
13150                haves: vec![have],
13151                shallow: vec![shallow],
13152                deepen: Some(10),
13153                deepen_since: Some(123456789),
13154                deepen_not: vec!["refs/tags/v1".into()],
13155                deepen_relative: true,
13156                filter: Some("blob:none".into()),
13157                packfile_uris: Some("http,https".into()),
13158                thin_pack: true,
13159                no_progress: true,
13160                include_tag: true,
13161                ofs_delta: true,
13162                sideband_all: true,
13163                wait_for_done: true,
13164                done: true,
13165            }
13166        );
13167        assert_eq!(
13168            request
13169                .to_command_request()
13170                .expect("test operation should succeed"),
13171            command
13172        );
13173    }
13174
13175    #[test]
13176    fn protocol_v2_fetch_request_rejects_malformed_arguments() {
13177        assert!(
13178            ProtocolV2FetchRequest::from_command_request(
13179                ObjectFormat::Sha1,
13180                &ProtocolV2CommandRequest {
13181                    command: "ls-refs".into(),
13182                    capabilities: Vec::new(),
13183                    arguments: Vec::new(),
13184                },
13185            )
13186            .is_err()
13187        );
13188        assert!(
13189            ProtocolV2FetchRequest::from_command_request(
13190                ObjectFormat::Sha1,
13191                &ProtocolV2CommandRequest {
13192                    command: "fetch".into(),
13193                    capabilities: Vec::new(),
13194                    arguments: vec![b"want not-an-oid".to_vec()],
13195                },
13196            )
13197            .is_err()
13198        );
13199        assert!(
13200            ProtocolV2FetchRequest::from_command_request(
13201                ObjectFormat::Sha1,
13202                &ProtocolV2CommandRequest {
13203                    command: "fetch".into(),
13204                    capabilities: Vec::new(),
13205                    arguments: vec![b"deepen 0".to_vec()],
13206                },
13207            )
13208            .is_err()
13209        );
13210        assert!(
13211            ProtocolV2FetchRequest::from_command_request(
13212                ObjectFormat::Sha1,
13213                &ProtocolV2CommandRequest {
13214                    command: "fetch".into(),
13215                    capabilities: Vec::new(),
13216                    arguments: vec![b"filter blob:none".to_vec(), b"filter tree:0".to_vec()],
13217                },
13218            )
13219            .is_err()
13220        );
13221        assert!(
13222            ProtocolV2FetchRequest {
13223                deepen: Some(0),
13224                ..ProtocolV2FetchRequest::default()
13225            }
13226            .to_command_request()
13227            .is_err()
13228        );
13229    }
13230
13231    #[test]
13232    fn protocol_v2_fetch_request_streams_round_trip() {
13233        let want = ObjectId::from_hex(
13234            ObjectFormat::Sha1,
13235            "1111111111111111111111111111111111111111",
13236        )
13237        .expect("test operation should succeed");
13238        let have = ObjectId::from_hex(
13239            ObjectFormat::Sha1,
13240            "2222222222222222222222222222222222222222",
13241        )
13242        .expect("test operation should succeed");
13243        let request = ProtocolV2FetchRequest {
13244            wants: vec![want],
13245            haves: vec![have],
13246            deepen: Some(5),
13247            filter: Some("blob:none".into()),
13248            thin_pack: true,
13249            done: true,
13250            ..ProtocolV2FetchRequest::default()
13251        };
13252        let mut encoded = Vec::new();
13253        write_protocol_v2_fetch_request(&mut encoded, &request)
13254            .expect("test operation should succeed");
13255        encoded.extend_from_slice(b"tail");
13256
13257        let mut input = encoded.as_slice();
13258        assert_eq!(
13259            read_protocol_v2_fetch_request(ObjectFormat::Sha1, &mut input)
13260                .expect("test operation should succeed"),
13261            request
13262        );
13263        assert_eq!(input, b"tail");
13264    }
13265
13266    #[test]
13267    fn protocol_v2_fetch_response_parses_and_encodes_sections() {
13268        let ack = ObjectId::from_hex(
13269            ObjectFormat::Sha1,
13270            "1111111111111111111111111111111111111111",
13271        )
13272        .expect("test operation should succeed");
13273        let shallow = ObjectId::from_hex(
13274            ObjectFormat::Sha1,
13275            "2222222222222222222222222222222222222222",
13276        )
13277        .expect("test operation should succeed");
13278        let wanted = ObjectId::from_hex(
13279            ObjectFormat::Sha1,
13280            "3333333333333333333333333333333333333333",
13281        )
13282        .expect("test operation should succeed");
13283        let pack_hash = ObjectId::from_hex(
13284            ObjectFormat::Sha1,
13285            "4444444444444444444444444444444444444444",
13286        )
13287        .expect("test operation should succeed");
13288        let frames = vec![
13289            PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13290            PktLineFrame::Data(b"ACK 1111111111111111111111111111111111111111\n".to_vec()),
13291            PktLineFrame::Data(b"ready\n".to_vec()),
13292            PktLineFrame::Delimiter,
13293            PktLineFrame::Data(b"shallow-info\n".to_vec()),
13294            PktLineFrame::Data(b"shallow 2222222222222222222222222222222222222222\n".to_vec()),
13295            PktLineFrame::Delimiter,
13296            PktLineFrame::Data(b"wanted-refs\n".to_vec()),
13297            PktLineFrame::Data(
13298                b"3333333333333333333333333333333333333333 refs/heads/main\n".to_vec(),
13299            ),
13300            PktLineFrame::Delimiter,
13301            PktLineFrame::Data(b"packfile-uris\n".to_vec()),
13302            PktLineFrame::Data(
13303                b"4444444444444444444444444444444444444444 https://example.invalid/pack-a.pack\n"
13304                    .to_vec(),
13305            ),
13306            PktLineFrame::Delimiter,
13307            PktLineFrame::Data(b"packfile\n".to_vec()),
13308            PktLineFrame::Data(b"\x01PACK bytes".to_vec()),
13309            PktLineFrame::Flush,
13310        ];
13311        let sections = parse_protocol_v2_fetch_response(ObjectFormat::Sha1, &frames)
13312            .expect("test operation should succeed");
13313        assert_eq!(
13314            sections,
13315            vec![
13316                ProtocolV2FetchResponseSection::Acknowledgments(vec![
13317                    ProtocolV2FetchAcknowledgment::Ack(ack),
13318                    ProtocolV2FetchAcknowledgment::Ready,
13319                ]),
13320                ProtocolV2FetchResponseSection::ShallowInfo(vec![
13321                    ProtocolV2FetchShallowInfo::Shallow(shallow)
13322                ]),
13323                ProtocolV2FetchResponseSection::WantedRefs(vec![ProtocolV2FetchWantedRef {
13324                    oid: wanted,
13325                    name: "refs/heads/main".into(),
13326                }]),
13327                ProtocolV2FetchResponseSection::PackfileUris(vec![ProtocolV2FetchPackfileUri {
13328                    pack_hash,
13329                    uri: "https://example.invalid/pack-a.pack".into(),
13330                }]),
13331                ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK bytes".to_vec()]),
13332            ]
13333        );
13334        assert_eq!(
13335            encode_protocol_v2_fetch_response(&sections).expect("test operation should succeed"),
13336            frames
13337        );
13338    }
13339
13340    #[test]
13341    fn protocol_v2_fetch_response_preserves_unknown_sections() {
13342        let frames = vec![
13343            PktLineFrame::Data(b"server-feature\n".to_vec()),
13344            PktLineFrame::Data(b"opaque line\n".to_vec()),
13345            PktLineFrame::Flush,
13346        ];
13347        let sections = parse_protocol_v2_fetch_response(ObjectFormat::Sha1, &frames)
13348            .expect("test operation should succeed");
13349        assert_eq!(
13350            sections,
13351            vec![ProtocolV2FetchResponseSection::Unknown {
13352                name: "server-feature".into(),
13353                lines: vec![b"opaque line\n".to_vec()],
13354            }]
13355        );
13356        assert_eq!(
13357            encode_protocol_v2_fetch_response(&sections).expect("test operation should succeed"),
13358            frames
13359        );
13360    }
13361
13362    #[test]
13363    fn protocol_v2_fetch_response_streams_round_trip() {
13364        let ack = ObjectId::from_hex(
13365            ObjectFormat::Sha1,
13366            "1111111111111111111111111111111111111111",
13367        )
13368        .expect("test operation should succeed");
13369        let sections = vec![
13370            ProtocolV2FetchResponseSection::Acknowledgments(vec![
13371                ProtocolV2FetchAcknowledgment::Ack(ack),
13372                ProtocolV2FetchAcknowledgment::Ready,
13373            ]),
13374            ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK bytes".to_vec()]),
13375        ];
13376        let mut encoded = Vec::new();
13377        write_protocol_v2_fetch_response(&mut encoded, &sections)
13378            .expect("test operation should succeed");
13379        encoded.extend_from_slice(b"tail");
13380
13381        let mut input = encoded.as_slice();
13382        assert_eq!(
13383            read_protocol_v2_fetch_response(ObjectFormat::Sha1, &mut input)
13384                .expect("test operation should succeed"),
13385            sections
13386        );
13387        assert_eq!(input, b"tail");
13388    }
13389
13390    #[test]
13391    fn protocol_v2_fetch_sideband_all_response_parses_sections_and_progress() {
13392        let frames = vec![
13393            PktLineFrame::Data(
13394                encode_sideband_packet(&SideBandPacket {
13395                    channel: SideBandChannel::Data,
13396                    data: b"acknowledgments\n".to_vec(),
13397                })
13398                .expect("test operation should succeed"),
13399            ),
13400            PktLineFrame::Data(
13401                encode_sideband_packet(&SideBandPacket {
13402                    channel: SideBandChannel::Data,
13403                    data: b"NAK\n".to_vec(),
13404                })
13405                .expect("test operation should succeed"),
13406            ),
13407            PktLineFrame::Data(
13408                encode_sideband_packet(&SideBandPacket {
13409                    channel: SideBandChannel::Progress,
13410                    data: b"keepalive\n".to_vec(),
13411                })
13412                .expect("test operation should succeed"),
13413            ),
13414            PktLineFrame::Delimiter,
13415            PktLineFrame::Data(
13416                encode_sideband_packet(&SideBandPacket {
13417                    channel: SideBandChannel::Data,
13418                    data: b"packfile\n".to_vec(),
13419                })
13420                .expect("test operation should succeed"),
13421            ),
13422            PktLineFrame::Data(b"\x01PACK".to_vec()),
13423            PktLineFrame::Data(b"\x02counting objects\n".to_vec()),
13424            PktLineFrame::Flush,
13425        ];
13426
13427        let response = parse_protocol_v2_fetch_sideband_all_response(ObjectFormat::Sha1, &frames)
13428            .expect("test operation should succeed");
13429        assert_eq!(
13430            response,
13431            ProtocolV2FetchSidebandAllResponse {
13432                sections: vec![
13433                    ProtocolV2FetchResponseSection::Acknowledgments(vec![
13434                        ProtocolV2FetchAcknowledgment::Nak
13435                    ]),
13436                    ProtocolV2FetchResponseSection::Packfile(vec![
13437                        b"\x01PACK".to_vec(),
13438                        b"\x02counting objects\n".to_vec(),
13439                    ]),
13440                ],
13441                progress: vec![b"keepalive\n".to_vec()],
13442            }
13443        );
13444        assert_eq!(
13445            demux_protocol_v2_fetch_packfile(&response.sections)
13446                .expect("test operation should succeed"),
13447            Some(SideBandDemux {
13448                data: b"PACK".to_vec(),
13449                progress: vec![b"counting objects\n".to_vec()],
13450            })
13451        );
13452    }
13453
13454    #[test]
13455    fn protocol_v2_fetch_sideband_all_response_streams_round_trip() {
13456        let sections = vec![
13457            ProtocolV2FetchResponseSection::Acknowledgments(vec![
13458                ProtocolV2FetchAcknowledgment::Nak,
13459            ]),
13460            ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK bytes".to_vec()]),
13461        ];
13462        let mut encoded = Vec::new();
13463        write_protocol_v2_fetch_sideband_all_response(&mut encoded, &sections)
13464            .expect("test operation should succeed");
13465        encoded.extend_from_slice(b"tail");
13466
13467        let mut input = encoded.as_slice();
13468        assert_eq!(
13469            read_protocol_v2_fetch_sideband_all_response(ObjectFormat::Sha1, &mut input)
13470                .expect("test operation should succeed"),
13471            ProtocolV2FetchSidebandAllResponse {
13472                sections: sections.clone(),
13473                progress: Vec::new(),
13474            }
13475        );
13476        assert_eq!(input, b"tail");
13477
13478        let mut encoded = Vec::new();
13479        write_protocol_v2_fetch_sideband_all_response_with_response_end(&mut encoded, &sections)
13480            .expect("test operation should succeed");
13481        encoded.extend_from_slice(b"tail");
13482
13483        let mut input = encoded.as_slice();
13484        assert_eq!(
13485            read_protocol_v2_fetch_sideband_all_response_until_response_end(
13486                ObjectFormat::Sha1,
13487                &mut input,
13488            )
13489            .expect("test operation should succeed")
13490            .sections,
13491            sections
13492        );
13493        assert_eq!(input, b"tail");
13494    }
13495
13496    #[test]
13497    fn protocol_v2_fetch_sideband_all_response_rejects_malformed_sideband() {
13498        assert!(
13499            parse_protocol_v2_fetch_sideband_all_response(
13500                ObjectFormat::Sha1,
13501                &[
13502                    PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13503                    PktLineFrame::Flush,
13504                ],
13505            )
13506            .is_err()
13507        );
13508        assert!(
13509            parse_protocol_v2_fetch_sideband_all_response(
13510                ObjectFormat::Sha1,
13511                &[
13512                    PktLineFrame::Data(
13513                        encode_sideband_packet(&SideBandPacket {
13514                            channel: SideBandChannel::Fatal,
13515                            data: b"remote died\n".to_vec(),
13516                        })
13517                        .expect("test operation should succeed"),
13518                    ),
13519                    PktLineFrame::Flush,
13520                ],
13521            )
13522            .is_err()
13523        );
13524    }
13525
13526    #[test]
13527    fn protocol_v2_object_info_response_parses_and_encodes_size_records() {
13528        let oid = ObjectId::from_hex(
13529            ObjectFormat::Sha1,
13530            "1111111111111111111111111111111111111111",
13531        )
13532        .expect("test operation should succeed");
13533        let frames = vec![
13534            PktLineFrame::Data(b"size\n".to_vec()),
13535            PktLineFrame::Data(b"1111111111111111111111111111111111111111 12345\n".to_vec()),
13536            PktLineFrame::Flush,
13537        ];
13538        let response = parse_protocol_v2_object_info_response(ObjectFormat::Sha1, &frames)
13539            .expect("test operation should succeed");
13540        assert_eq!(
13541            response,
13542            ProtocolV2ObjectInfoResponse {
13543                size: true,
13544                records: vec![ProtocolV2ObjectInfoRecord { oid, size: 12345 }],
13545            }
13546        );
13547        assert_eq!(
13548            encode_protocol_v2_object_info_response(&response)
13549                .expect("test operation should succeed"),
13550            frames
13551        );
13552    }
13553
13554    #[test]
13555    fn protocol_v2_object_info_response_streams_and_exchanges() {
13556        let request = ProtocolV2ObjectInfoRequest {
13557            size: true,
13558            oids: vec![
13559                ObjectId::from_hex(
13560                    ObjectFormat::Sha1,
13561                    "1111111111111111111111111111111111111111",
13562                )
13563                .expect("test operation should succeed"),
13564            ],
13565        };
13566        let response = ProtocolV2ObjectInfoResponse {
13567            size: true,
13568            records: vec![ProtocolV2ObjectInfoRecord {
13569                oid: request.oids[0].clone(),
13570                size: 7,
13571            }],
13572        };
13573
13574        let mut encoded = Vec::new();
13575        write_protocol_v2_object_info_response(&mut encoded, &response)
13576            .expect("test operation should succeed");
13577        encoded.extend_from_slice(b"tail");
13578        let mut input = encoded.as_slice();
13579        assert_eq!(
13580            read_protocol_v2_object_info_response(ObjectFormat::Sha1, &mut input)
13581                .expect("test operation should succeed"),
13582            response
13583        );
13584        assert_eq!(input, b"tail");
13585
13586        let mut response_bytes = Vec::new();
13587        write_protocol_v2_object_info_response(&mut response_bytes, &response)
13588            .expect("test operation should succeed");
13589        let mut input = response_bytes.as_slice();
13590        let mut output = Vec::new();
13591        assert_eq!(
13592            exchange_protocol_v2_object_info(
13593                ObjectFormat::Sha1,
13594                &mut input,
13595                &mut output,
13596                &request,
13597            )
13598            .expect("test operation should succeed"),
13599            response
13600        );
13601        assert!(input.is_empty());
13602        let mut output_read = output.as_slice();
13603        assert_eq!(
13604            read_protocol_v2_object_info_request(ObjectFormat::Sha1, &mut output_read)
13605                .expect("test operation should succeed"),
13606            request
13607        );
13608    }
13609
13610    #[test]
13611    fn protocol_v2_object_info_response_rejects_malformed_records() {
13612        assert!(parse_protocol_v2_object_info_response(ObjectFormat::Sha1, &[]).is_err());
13613        assert!(
13614            parse_protocol_v2_object_info_response(
13615                ObjectFormat::Sha1,
13616                &[PktLineFrame::Data(b"size\n".to_vec())],
13617            )
13618            .is_err()
13619        );
13620        assert!(
13621            parse_protocol_v2_object_info_response(
13622                ObjectFormat::Sha1,
13623                &[PktLineFrame::Data(b"type\n".to_vec()), PktLineFrame::Flush,],
13624            )
13625            .is_err()
13626        );
13627        assert!(
13628            parse_protocol_v2_object_info_response(
13629                ObjectFormat::Sha1,
13630                &[
13631                    PktLineFrame::Data(b"size\n".to_vec()),
13632                    PktLineFrame::Data(
13633                        b"1111111111111111111111111111111111111111 not-a-size\n".to_vec()
13634                    ),
13635                    PktLineFrame::Flush,
13636                ],
13637            )
13638            .is_err()
13639        );
13640        assert!(
13641            parse_protocol_v2_object_info_response(
13642                ObjectFormat::Sha1,
13643                &[
13644                    PktLineFrame::Data(b"size\n".to_vec()),
13645                    PktLineFrame::Delimiter,
13646                    PktLineFrame::Flush,
13647                ],
13648            )
13649            .is_err()
13650        );
13651        assert!(
13652            encode_protocol_v2_object_info_response(&ProtocolV2ObjectInfoResponse {
13653                size: false,
13654                records: Vec::new(),
13655            })
13656            .is_err()
13657        );
13658    }
13659
13660    #[test]
13661    fn protocol_v2_fetch_response_reads_stateless_response_end() {
13662        let sections = vec![ProtocolV2FetchResponseSection::Acknowledgments(vec![
13663            ProtocolV2FetchAcknowledgment::Nak,
13664        ])];
13665        let mut encoded = Vec::new();
13666        write_protocol_v2_fetch_response_with_response_end(&mut encoded, &sections)
13667            .expect("test operation should succeed");
13668        encoded.extend_from_slice(b"tail");
13669
13670        let mut input = encoded.as_slice();
13671        assert_eq!(
13672            read_protocol_v2_fetch_response_until_response_end(ObjectFormat::Sha1, &mut input)
13673                .expect("test operation should succeed"),
13674            sections
13675        );
13676        assert_eq!(input, b"tail");
13677        assert!(
13678            parse_protocol_v2_fetch_response(
13679                ObjectFormat::Sha1,
13680                &[
13681                    PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13682                    PktLineFrame::ResponseEnd,
13683                ],
13684            )
13685            .is_err()
13686        );
13687    }
13688
13689    #[test]
13690    fn protocol_v2_fetch_exchange_writes_request_and_reads_response() {
13691        let want = ObjectId::from_hex(
13692            ObjectFormat::Sha1,
13693            "1111111111111111111111111111111111111111",
13694        )
13695        .expect("test operation should succeed");
13696        let request = ProtocolV2FetchRequest {
13697            wants: vec![want],
13698            thin_pack: true,
13699            done: true,
13700            ..ProtocolV2FetchRequest::default()
13701        };
13702        let sections = vec![ProtocolV2FetchResponseSection::Acknowledgments(vec![
13703            ProtocolV2FetchAcknowledgment::Nak,
13704        ])];
13705        let mut response = Vec::new();
13706        write_protocol_v2_fetch_response(&mut response, &sections)
13707            .expect("test operation should succeed");
13708
13709        let mut input = response.as_slice();
13710        let mut output = Vec::new();
13711        assert_eq!(
13712            exchange_protocol_v2_fetch(ObjectFormat::Sha1, &mut input, &mut output, &request)
13713                .expect("test operation should succeed"),
13714            sections
13715        );
13716        assert!(input.is_empty());
13717        let mut output_read = output.as_slice();
13718        assert_eq!(
13719            read_protocol_v2_fetch_request(ObjectFormat::Sha1, &mut output_read)
13720                .expect("test operation should succeed"),
13721            request
13722        );
13723    }
13724
13725    #[test]
13726    fn protocol_v2_fetch_packfile_demuxes_sideband_section() {
13727        let sections = vec![
13728            ProtocolV2FetchResponseSection::Acknowledgments(vec![
13729                ProtocolV2FetchAcknowledgment::Nak,
13730            ]),
13731            ProtocolV2FetchResponseSection::Packfile(vec![
13732                b"\x01PACK".to_vec(),
13733                b"\x02counting objects\n".to_vec(),
13734                b"\x01 bytes".to_vec(),
13735                b"\x02done\n".to_vec(),
13736            ]),
13737        ];
13738
13739        assert_eq!(
13740            demux_protocol_v2_fetch_packfile(&sections).expect("test operation should succeed"),
13741            Some(SideBandDemux {
13742                data: b"PACK bytes".to_vec(),
13743                progress: vec![b"counting objects\n".to_vec(), b"done\n".to_vec()],
13744            })
13745        );
13746        assert_eq!(
13747            demux_protocol_v2_fetch_packfile(&[ProtocolV2FetchResponseSection::Acknowledgments(
13748                vec![ProtocolV2FetchAcknowledgment::Nak],
13749            )])
13750            .expect("test operation should succeed"),
13751            None
13752        );
13753    }
13754
13755    #[test]
13756    fn protocol_v2_fetch_packfile_demux_rejects_duplicate_or_bad_sideband() {
13757        assert!(
13758            demux_protocol_v2_fetch_packfile(&[
13759                ProtocolV2FetchResponseSection::Packfile(vec![b"\x01PACK".to_vec()]),
13760                ProtocolV2FetchResponseSection::Packfile(vec![b"\x01more".to_vec()]),
13761            ])
13762            .is_err()
13763        );
13764        assert!(
13765            demux_protocol_v2_fetch_packfile(&[ProtocolV2FetchResponseSection::Packfile(vec![
13766                b"\x03remote died\n".to_vec()
13767            ])])
13768            .is_err()
13769        );
13770        assert!(
13771            demux_protocol_v2_fetch_packfile(&[ProtocolV2FetchResponseSection::Packfile(vec![
13772                b"\x04bad".to_vec()
13773            ])])
13774            .is_err()
13775        );
13776    }
13777
13778    #[test]
13779    fn protocol_v2_fetch_response_rejects_malformed_sections() {
13780        assert!(
13781            parse_protocol_v2_fetch_response(
13782                ObjectFormat::Sha1,
13783                &[PktLineFrame::Data(b"acknowledgments\n".to_vec())],
13784            )
13785            .is_err()
13786        );
13787        assert!(
13788            parse_protocol_v2_fetch_response(
13789                ObjectFormat::Sha1,
13790                &[PktLineFrame::Delimiter, PktLineFrame::Flush],
13791            )
13792            .is_err()
13793        );
13794        assert!(
13795            parse_protocol_v2_fetch_response(
13796                ObjectFormat::Sha1,
13797                &[
13798                    PktLineFrame::Data(b"acknowledgments\n".to_vec()),
13799                    PktLineFrame::Data(b"ACK not-an-oid\n".to_vec()),
13800                    PktLineFrame::Flush,
13801                ],
13802            )
13803            .is_err()
13804        );
13805        assert!(
13806            parse_protocol_v2_fetch_response(
13807                ObjectFormat::Sha1,
13808                &[
13809                    PktLineFrame::Data(b"packfile-uris\n".to_vec()),
13810                    PktLineFrame::Data(b"https://example.invalid/pack-a.pack\n".to_vec()),
13811                    PktLineFrame::Flush,
13812                ],
13813            )
13814            .is_err()
13815        );
13816        assert!(
13817            parse_protocol_v2_fetch_response(
13818                ObjectFormat::Sha1,
13819                &[
13820                    PktLineFrame::Data(b"packfile-uris\n".to_vec()),
13821                    PktLineFrame::Data(
13822                        b"not-a-hash https://example.invalid/pack-a.pack\n".to_vec()
13823                    ),
13824                    PktLineFrame::Flush,
13825                ],
13826            )
13827            .is_err()
13828        );
13829        assert!(
13830            encode_protocol_v2_fetch_response(&[ProtocolV2FetchResponseSection::WantedRefs(vec![
13831                ProtocolV2FetchWantedRef {
13832                    oid: ObjectId::from_hex(
13833                        ObjectFormat::Sha1,
13834                        "1111111111111111111111111111111111111111",
13835                    )
13836                    .expect("test operation should succeed"),
13837                    name: "bad ref".into(),
13838                }
13839            ])])
13840            .is_err()
13841        );
13842    }
13843
13844    #[test]
13845    fn protocol_v2_ls_refs_response_bridges_into_ref_advertisement_set() {
13846        let head = ObjectId::from_hex(
13847            ObjectFormat::Sha1,
13848            "1111111111111111111111111111111111111111",
13849        )
13850        .expect("test operation should succeed");
13851        let tag = ObjectId::from_hex(
13852            ObjectFormat::Sha1,
13853            "2222222222222222222222222222222222222222",
13854        )
13855        .expect("test operation should succeed");
13856        let tag_peeled = ObjectId::from_hex(
13857            ObjectFormat::Sha1,
13858            "3333333333333333333333333333333333333333",
13859        )
13860        .expect("test operation should succeed");
13861        let frames = vec![
13862            PktLineFrame::Data(
13863                b"1111111111111111111111111111111111111111 HEAD symref-target:refs/heads/main\n"
13864                    .to_vec(),
13865            ),
13866            PktLineFrame::Data(
13867                b"1111111111111111111111111111111111111111 refs/heads/main\n".to_vec(),
13868            ),
13869            PktLineFrame::Data(
13870                b"2222222222222222222222222222222222222222 refs/tags/v1 peeled:3333333333333333333333333333333333333333\n"
13871                    .to_vec(),
13872            ),
13873            PktLineFrame::Flush,
13874        ];
13875
13876        let set = parse_protocol_v2_ls_refs_response_as_ref_advertisement_set(
13877            ObjectFormat::Sha1,
13878            &frames,
13879        )
13880        .expect("test operation should succeed");
13881        assert_eq!(
13882            set,
13883            RefAdvertisementSet {
13884                protocol: ProtocolVersion::V2,
13885                refs: vec![
13886                    RefAdvertisement {
13887                        oid: head.clone(),
13888                        name: "HEAD".into(),
13889                        capabilities: vec![Capability {
13890                            name: "symref".into(),
13891                            value: Some("HEAD:refs/heads/main".into()),
13892                        }],
13893                    },
13894                    RefAdvertisement {
13895                        oid: head,
13896                        name: "refs/heads/main".into(),
13897                        capabilities: Vec::new(),
13898                    },
13899                    RefAdvertisement {
13900                        oid: tag,
13901                        name: "refs/tags/v1".into(),
13902                        capabilities: Vec::new(),
13903                    },
13904                    RefAdvertisement {
13905                        oid: tag_peeled,
13906                        name: "refs/tags/v1^{}".into(),
13907                        capabilities: Vec::new(),
13908                    },
13909                ],
13910                shallow: Vec::new(),
13911            }
13912        );
13913
13914        // The streaming reader path produces the same bridged set.
13915        let mut encoded = Vec::new();
13916        write_pkt_line_frames(&mut encoded, &frames).expect("test operation should succeed");
13917        encoded.extend_from_slice(b"tail");
13918        let mut input = encoded.as_slice();
13919        assert_eq!(
13920            read_protocol_v2_ls_refs_response_as_ref_advertisement_set(
13921                ObjectFormat::Sha1,
13922                &mut input,
13923            )
13924            .expect("test operation should succeed"),
13925            set,
13926        );
13927        assert_eq!(input, b"tail");
13928    }
13929
13930    #[test]
13931    fn protocol_v2_ls_refs_records_bridge_unborn_head_symref_and_empty() {
13932        // An unborn HEAD pointing at an as-yet-uncreated branch carries only a
13933        // symref capability and has no concrete ref to attach it to.
13934        let records = vec![ProtocolV2LsRefsRecord::Unborn {
13935            name: "HEAD".into(),
13936            symref_target: Some("refs/heads/main".into()),
13937            attributes: Vec::new(),
13938        }];
13939        assert!(protocol_v2_ls_refs_records_to_ref_advertisement_set(&records).is_err());
13940
13941        // An empty ls-refs response bridges to an empty v2 set.
13942        assert_eq!(
13943            protocol_v2_ls_refs_records_to_ref_advertisement_set(&[])
13944                .expect("test operation should succeed"),
13945            RefAdvertisementSet {
13946                protocol: ProtocolVersion::V2,
13947                refs: Vec::new(),
13948                shallow: Vec::new(),
13949            }
13950        );
13951
13952        // An unborn HEAD alongside a concrete ref attaches the symref to the
13953        // first ref, matching the v0/v1 advertisement convention.
13954        let main = ObjectId::from_hex(
13955            ObjectFormat::Sha1,
13956            "4444444444444444444444444444444444444444",
13957        )
13958        .expect("test operation should succeed");
13959        let records = vec![
13960            ProtocolV2LsRefsRecord::Unborn {
13961                name: "HEAD".into(),
13962                symref_target: Some("refs/heads/main".into()),
13963                attributes: Vec::new(),
13964            },
13965            ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
13966                oid: main.clone(),
13967                name: "refs/heads/main".into(),
13968                peeled: None,
13969                symref_target: None,
13970                attributes: Vec::new(),
13971            }),
13972        ];
13973        let set = protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
13974            .expect("test operation should succeed");
13975        assert_eq!(
13976            set,
13977            RefAdvertisementSet {
13978                protocol: ProtocolVersion::V2,
13979                refs: vec![RefAdvertisement {
13980                    oid: main,
13981                    name: "refs/heads/main".into(),
13982                    capabilities: vec![Capability {
13983                        name: "symref".into(),
13984                        value: Some("HEAD:refs/heads/main".into()),
13985                    }],
13986                }],
13987                shallow: Vec::new(),
13988            }
13989        );
13990    }
13991}