Skip to main content

gunnar_wire/
fetch_pack.rs

1//! **The other chair at the table**: the client seat of `git fetch`, protocol
2//! v2, with no `gix` and no filesystem.
3//!
4//! [`crate::upload_pack`] answers a fetch. This asks one. The two are the same
5//! conversation read from opposite ends, and they share every piece of it that
6//! can be shared: the pkt-line framer ([`crate::wire_io`]), the capability and
7//! argument tables ([`crate::caps`] — the *same* tables the server parses
8//! against, so a client cannot send an argument the server would reject), the
9//! advertised-ref vocabulary ([`AdvertisedRef`]), and the packfile sink
10//! ([`PackIngest`]) that every `git push` already lands through.
11//!
12//! # Why this is written here and not delegated to `gix-protocol`
13//!
14//! MEASURED 2026-09-08 on this workspace, with
15//! `cargo tree -p gunnar-wire -e normal --prefix none | sed 's/ v[0-9].*//' | sort -u | wc -l`
16//! (distinct crate *names*; the raw line count is 51 because five crates resolve
17//! at two versions):
18//!
19//! | | crates in the normal graph |
20//! |---|---|
21//! | `gunnar-wire` before this module | **46** |
22//! | `gunnar-wire` after it | **46** — the same 46, and the same three `gix-*`: `gix-hash`, `gix-trace`, `gix-features` |
23//! | `gix-protocol` standalone | **184** |
24//! | what delegating would have added | **+128**, and HTTP is a *further* feature (`http-client-curl` / `http-client-reqwest`) |
25//!
26//! There is **zero curl in the constellation today**, and this module keeps it
27//! at zero. The only `gix` crate it touches is `gix-hash` — a 32-byte array and
28//! a two-variant enum, which is gunnar's object-id vocabulary and was already in
29//! the graph.
30//!
31//! The count is not the whole argument, and on its own it would be a weak one.
32//! The other half is that gix's porcelain does not fit the machine this runs on:
33//! `gunnar-client`'s `fetch.rs` names `gix::` **60** times — ten of them
34//! `gix::Repository` — and needs a `.git` directory and a worktree. The appliance has
35//! neither — it runs the **znippy** engine (Arrow-IPC, objects *and* refs) as
36//! PID 1 with no filesystem underneath. A fetch that must first materialise a
37//! repository on disk cannot run there at all, at any dependency count.
38//!
39//! And the work that delegation would have saved is work this crate had already
40//! done: `upload_pack.rs` answers a fetch, `negotiate.rs` runs the `have`/`want`
41//! dance, `receive_pack.rs` accepts a pack, `refs.rs` advertises, `wire_io.rs`
42//! frames. What was missing was the other seat, not the protocol.
43//!
44//! # This module does not store objects, and it does not write references
45//!
46//! Two deliberate holes, and both are the same principle.
47//!
48//! **Objects** go to a caller-supplied [`PackIngest`] — the identical trait
49//! `receive_pack` hands a *pushed* pack to, because **a fetched pack and a
50//! pushed pack are the same bytes**. znippy implements it once and both
51//! directions land through it. Writing a second sink here would have been a
52//! second storage authority, and two authorities disagree (one implementation).
53//!
54//! **References** are *planned* and not applied: [`FetchReport::updates`] says
55//! what would move, and the caller — `gunnar-server`, holding the store and the
56//! policy gates — decides whether it moves. A fetch that wrote refs from inside
57//! this crate would apply a mirror's whole plan with no gate, no atomicity
58//! across the batch, and no way for an operator to see it first.
59//!
60//! # What it streams, and what it therefore never holds
61//!
62//! The packfile is handed to [`PackIngest::ingest`] as a `&mut dyn BufRead` that
63//! is demultiplexed from the server's sideband **one pkt-line at a time**. The
64//! most this module ever holds of a packfile is one 65 515-byte sideband
65//! payload, whatever the size of the repository. A mirror of a large repository
66//! that required the pack in RAM would be a mirror that cannot run on the
67//! appliance, which is the only place this is going.
68//!
69//! # Two rounds, at most, and why that is enough
70//!
71//! Round one sends every want and **every local tip** as a `have`, and reads the
72//! `acknowledgments` section. If the server says `ready` the packfile follows in
73//! the same response and there is no round two. Otherwise round two repeats the
74//! request with `done`, and the server must answer with a pack.
75//!
76//! That is the *simple, correct* form, deliberately: git's client walks its own
77//! history backwards over many rounds because it wants to find a **near** cut
78//! point without sending its entire ref list. gunnar's have-source is a mirror's
79//! store, whose tips are already the complete offer — there is no round three to
80//! send, because there is nothing left to say. Multi-round negotiation is a
81//! measurement away, not a design away; nothing here would have to change shape
82//! to grow it, only [`FetchPack::fetch`]'s loop.
83//!
84//! # Protocol v2 only
85//!
86//! Named, not implied. The server half here speaks v2 **and** a v0 fallback,
87//! because stock `git` negotiates v0 whenever `GIT_PROTOCOL` does not reach the
88//! server process and a server that could not answer those clients would simply
89//! not work. A *client* has the opposite freedom: it chooses. Every remote
90//! gunnar fetches from — gunnar itself, GitHub, Codeberg — speaks v2, so a v0
91//! client path would be a second implementation of this file that nothing runs.
92//! If a remote is ever found that refuses v2, this is where the fallback goes.
93
94use std::io::{BufRead, Write};
95
96use gix_hash::{Kind, ObjectId};
97use gunnar_pktline::{Packet, BAND_ERROR, BAND_PACK, BAND_PROGRESS};
98
99use crate::caps::{self, Argument, PeerCapabilities};
100use crate::limits::{Budget, DEFAULT_REQUEST_LIMIT_BYTES};
101use crate::receive_pack::{PackIngest, PackReceipt};
102use crate::upload_pack::AdvertisedRef;
103use crate::wire_io::{self, read_packet};
104use crate::{Error, Result};
105
106/// The reference namespace a remote advertised, and what it said it is.
107#[derive(Debug, Clone)]
108pub struct Advertisement {
109    /// The hash algorithm the remote named in `object-format`, already checked
110    /// against the caller's own. See [`ls_remote`].
111    pub hash_kind: Kind,
112    /// Everything the remote advertised about itself, verbatim, for a caller
113    /// that wants to ask a question this module did not.
114    pub capabilities: PeerCapabilities,
115    /// Every reference `ls-refs` returned, in the order the remote sent them.
116    ///
117    /// [`AdvertisedRef`] and not a new struct: the server side already has
118    /// this exact shape — name, target, peeled, symref target — and a
119    /// client-side twin of it would be one more pair of types to keep agreeing
120    /// (one implementation).
121    pub refs: Vec<AdvertisedRef>,
122}
123
124/// One reference this fetch would move, and what it would move it from.
125///
126/// **A plan, not an action.** Nothing in this crate applies it; see the module
127/// doc for why the caller does.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct RefUpdatePlan {
130    /// The full reference name, as the remote advertised it.
131    pub name: String,
132    /// What the local store holds for that name now, or `None` if it holds
133    /// nothing — a new branch, or a first mirror.
134    pub old: Option<ObjectId>,
135    /// What the remote holds for it.
136    pub new: ObjectId,
137}
138
139/// What one fetch did.
140#[derive(Debug, Clone)]
141pub struct FetchReport {
142    /// The hash kind the exchange ran at.
143    pub hash_kind: Kind,
144    /// Every reference the remote advertised, whether or not it was wanted.
145    pub advertised: Vec<AdvertisedRef>,
146    /// The references the caller's plan chose, and where they would move.
147    pub updates: Vec<RefUpdatePlan>,
148    /// What the [`PackIngest`] said, or `None` when there was nothing to fetch.
149    ///
150    /// `None` is the ordinary answer for a mirror polling a remote that has not
151    /// moved: no `want`, no `command=fetch`, no pack, and no walk charged to
152    /// the server.
153    pub receipt: Option<PackReceipt>,
154    /// **The object count the packfile declared**, read from bytes 8..12 of its
155    /// header before a single object was stored.
156    ///
157    /// This is the denominator a progress bar needs, and it is available at the
158    /// *start* of the transfer rather than the end. Zero when no pack was
159    /// fetched.
160    pub objects_total: u32,
161    /// **The object count the ingest actually indexed**, from the receipt.
162    ///
163    /// Equal to [`Self::objects_total`] or the fetch fails; see
164    /// [`FetchPack::fetch`]. The numerator's *live* progress belongs to the
165    /// ingest and not to this module: counting objects as they pass means
166    /// inflating every one of them, which is a whole second pack decoder, and
167    /// gunnar already has exactly one (one implementation). What this module can report
168    /// live — and does, through [`FetchProgress`] — is the declared total the
169    /// moment the header arrives, and the byte count as it streams.
170    pub objects_done: u32,
171    /// Packfile bytes handed to the ingest, header and trailer included.
172    pub pack_bytes: u64,
173    /// How many `have` lines were offered.
174    pub haves_sent: usize,
175    /// Every `ACK <oid>` the remote sent, in order.
176    pub acked: Vec<ObjectId>,
177    /// How many `command=fetch` rounds were spent. Zero when nothing was
178    /// wanted, one when the remote was `ready` immediately, otherwise two.
179    pub rounds: usize,
180}
181
182/// Where a transfer reports itself while it is still running.
183///
184/// A queue renders a percentage from [`Self::objects_total`] as the
185/// denominator; the numerator is the ingest's to publish, because it is the
186/// ingest that knows how many objects it has taken in (see
187/// [`FetchReport::objects_done`]).
188pub trait FetchProgress {
189    /// The packfile header has arrived and declares this many objects. Called
190    /// exactly once per pack, before any of it reaches the ingest.
191    fn objects_total(&self, objects: u32);
192    /// This many packfile bytes have been handed on so far. Called as the
193    /// stream is consumed, never more than once per sideband packet.
194    fn pack_bytes(&self, streamed: u64);
195}
196
197/// The local half of a fetch: what this store already has.
198///
199/// One method, because one answer serves both questions the protocol asks of
200/// the local side: the oids are the `have` list, and the names are what makes
201/// [`RefUpdatePlan::old`] a real old value rather than a guess.
202///
203/// `gunnar-server` implements this over
204/// [`GitOps::refs`](gunnar_store::git_storage_trait::GitOps::refs) — one call,
205/// one snapshot — and a test implements it over a `Vec`.
206pub trait LocalRefs {
207    /// Every local reference and the object it resolves to, `HEAD` excluded.
208    ///
209    /// A symbolic reference should be resolved before it gets here; an unborn
210    /// one should simply be absent. Both are questions
211    /// [`RefSnapshot`](crate::RefSnapshot) already answers for a caller holding
212    /// a store.
213    fn tips(&self) -> Result<Vec<(String, ObjectId)>>;
214}
215
216impl LocalRefs for Vec<(String, ObjectId)> {
217    fn tips(&self) -> Result<Vec<(String, ObjectId)>> {
218        Ok(self.clone())
219    }
220}
221
222/// The ref plan a mirror uses: `refs/heads/*` and `refs/tags/*`.
223///
224/// A plain function rather than a configuration format, because a ref plan is a
225/// predicate and gunnar already has one config surface too many. A caller that
226/// wants something else passes its own closure to [`FetchPack::new`]; a caller
227/// that wants a mirror passes this.
228///
229/// Note what it excludes: `HEAD`, which every remote advertises and which no
230/// mirror should copy as a reference — it is a symbolic pointer into
231/// `refs/heads/`, and setting it is a separate decision the caller makes with
232/// [`Advertisement::refs`] in hand.
233pub fn mirror(name: &str) -> bool {
234    name.starts_with("refs/heads/") || name.starts_with("refs/tags/")
235}
236
237/// **`git ls-remote`, with no repository and no gix.**
238///
239/// Reads the remote's v2 capability advertisement, asks `command=ls-refs`, and
240/// returns what came back. It is the first two steps of [`FetchPack::fetch`]
241/// and shares their code exactly; it is a separate function because listing a
242/// remote's references is a thing `gunnar-server` needs to do *without*
243/// fetching, and until this existed that answer was locked inside gix porcelain
244/// that wants a `.git` directory to exist first.
245///
246/// `hash_kind` is what the **caller** is, and a remote that answers with a
247/// different `object-format` is refused by name
248/// ([`Error::RemoteObjectFormat`]). That refusal is object-format negotiation seen from the
249/// client's chair: a sha1 client against a sha256 remote does not fail, it
250/// negotiates an *empty* answer, and the operator is left with a working
251/// command that copied nothing.
252pub fn ls_remote(
253    hash_kind: Kind,
254    input: &mut dyn BufRead,
255    output: &mut dyn Write,
256) -> Result<Advertisement> {
257    let capabilities = greet(hash_kind, input)?;
258    let agent = caps::default_agent();
259    ask_ls_refs(hash_kind, &agent, &capabilities, output)?;
260    output.flush()?;
261    let refs = read_ref_listing(input)?;
262    Ok(Advertisement {
263        hash_kind,
264        capabilities,
265        refs,
266    })
267}
268
269/// **`git ls-remote` that takes the remote's object format instead of
270/// insisting on one** (2026-10-18).
271///
272/// For the one caller that has no repository yet and so no format of its own:
273/// a mirror import, which creates its repository at whatever the upstream is.
274/// The format is the upstream's `object-format` capability, and its absence is
275/// sha1 — the protocol's rule, since sha1 servers predate the capability. The
276/// returned [`Advertisement::hash_kind`] is that format, and the listing
277/// (including `HEAD`'s `symref-target`) was asked at it.
278pub fn ls_remote_any(input: &mut dyn BufRead, output: &mut dyn Write) -> Result<Advertisement> {
279    let capabilities = read_capabilities(input)?;
280    let hash_kind = match capabilities.value("object-format") {
281        None => Kind::Sha1,
282        Some(name) => {
283            caps::object_format_from_name(name).ok_or_else(|| Error::RemoteObjectFormat {
284                ours: "sha1 or sha256",
285                theirs: name.to_owned(),
286            })?
287        }
288    };
289    let agent = caps::default_agent();
290    ask_ls_refs(hash_kind, &agent, &capabilities, output)?;
291    output.flush()?;
292    let refs = read_ref_listing(input)?;
293    Ok(Advertisement {
294        hash_kind,
295        capabilities,
296        refs,
297    })
298}
299
300/// The client side of `git-upload-pack`, configured for one fetch.
301///
302/// Shaped like [`UploadPack`](crate::upload_pack::UploadPack) on purpose: a
303/// constructor taking the two things that cannot be defaulted, `with_*` for the
304/// rest, and one method that runs the whole conversation over a
305/// `&mut dyn BufRead` / `&mut dyn Write` pair.
306///
307/// # The transport pair is two arguments, not an `IoPair`
308///
309/// `gunnar-client` has a 57-line `IoPair<R, W>` and a `Transport` trait, and
310/// copying either here would have been a second seam for the same idea. This
311/// crate's seam is already the pair: every entry point in `upload_pack` and
312/// `receive_pack` takes `&mut dyn BufRead` and `&mut dyn Write` and cannot tell
313/// an SSH channel from a pipe from a `Vec<u8>`. A caller holding an `IoPair`
314/// calls `io()` and passes the two halves; a caller holding a russh channel
315/// passes the bridge `gunnar-server` already builds for `upload_pack`.
316pub struct FetchPack<'a> {
317    hash_kind: Kind,
318    agent: String,
319    wanted: &'a dyn Fn(&str) -> bool,
320    local: &'a dyn LocalRefs,
321    progress: Option<&'a dyn FetchProgress>,
322    /// The byte ceiling the packfile is held to, if any. See
323    /// [`FetchPack::with_ceiling`].
324    ceiling: Option<crate::receive_pack::PackCeiling>,
325}
326
327impl<'a> FetchPack<'a> {
328    /// A fetch at `hash_kind`, taking the references `wanted` accepts and
329    /// offering everything `local` holds as `have`s.
330    pub fn new(
331        hash_kind: Kind,
332        wanted: &'a dyn Fn(&str) -> bool,
333        local: &'a dyn LocalRefs,
334    ) -> Self {
335        FetchPack {
336            hash_kind,
337            agent: caps::default_agent(),
338            wanted,
339            local,
340            progress: None,
341            ceiling: None,
342        }
343    }
344
345    /// **Hold the packfile to a byte ceiling**, exactly as a push is held to
346    /// one: the stream is counted as the ingest takes it, cut the moment it
347    /// passes [`PackCeiling::bytes`](crate::PackCeiling::bytes) — the ingest
348    /// is handed at most the ceiling and one byte — and the fetch then fails
349    /// with [`Error::OverCeiling`] carrying the ceiling's own refusal. A
350    /// ledger on the ceiling is asked chunk by chunk, so a fetch and a push
351    /// into one namespace share its room rather than each spending it.
352    ///
353    /// This is how a mirror into a namespace with a storage cap stops cleanly
354    /// AT the cap when its upstream turns out bigger than the room it has: the
355    /// git protocol says nothing of a pack's size before it arrives, so the
356    /// only place the answer can be enforced is here, while it streams.
357    pub fn with_ceiling(mut self, ceiling: crate::receive_pack::PackCeiling) -> Self {
358        self.ceiling = Some(ceiling);
359        self
360    }
361
362    /// Announce a different `agent=` string.
363    pub fn with_agent(mut self, agent: String) -> Self {
364        self.agent = agent;
365        self
366    }
367
368    /// Report the declared object count and the streaming byte count as they
369    /// become known.
370    pub fn with_progress(mut self, progress: &'a dyn FetchProgress) -> Self {
371        self.progress = Some(progress);
372        self
373    }
374
375    /// The hash kind this client speaks, and the only `object-format` it will
376    /// accept from a remote.
377    pub fn hash_kind(&self) -> Kind {
378        self.hash_kind
379    }
380
381    /// **Run the whole fetch.**
382    ///
383    /// Advertisement, `ls-refs`, want selection, negotiation, and the packfile
384    /// streamed into `ingest`. Returns the plan; **applying the references is
385    /// the caller's job** (module doc).
386    ///
387    /// # What it refuses, and why each refusal is not a warning
388    ///
389    /// * a remote whose `object-format` is not this client's, and the
390    ///   alternative is an empty answer that reads as success;
391    /// * a `packfile` section that ends before its flush-pkt — a mirror that
392    ///   accepted half a pack would then apply references pointing at objects it
393    ///   does not hold;
394    /// * a pack whose header declares one object count and whose ingest reports
395    ///   another. An indexer that stops early and reports what it managed is
396    ///   indistinguishable from a complete transfer at every other seam; the
397    ///   declared count is the only independent statement of how many objects
398    ///   were supposed to arrive, and comparing them costs one `u32`.
399    pub fn fetch(
400        &self,
401        ingest: &dyn PackIngest,
402        input: &mut dyn BufRead,
403        output: &mut dyn Write,
404    ) -> Result<FetchReport> {
405        let capabilities = greet(self.hash_kind, input)?;
406        ask_ls_refs(self.hash_kind, &self.agent, &capabilities, output)?;
407        output.flush()?;
408        let advertised = read_ref_listing(input)?;
409
410        // One snapshot of the local side for the whole fetch: the `have` list
411        // and every `old` value in the plan come from the same call, so a
412        // reference that moves under us cannot make the plan promise an old
413        // value the store never held.
414        let local = self.local.tips()?;
415        let (updates, wants) = self.plan(&advertised, &local);
416
417        let mut report = FetchReport {
418            hash_kind: self.hash_kind,
419            advertised,
420            updates,
421            receipt: None,
422            objects_total: 0,
423            objects_done: 0,
424            pack_bytes: 0,
425            haves_sent: 0,
426            acked: Vec::new(),
427            rounds: 0,
428        };
429
430        if wants.is_empty() {
431            // Nothing to ask for. Sending `command=fetch` anyway would charge
432            // the remote a whole selection walk to produce an empty pack, which
433            // is what a mirror polling an idle remote would pay on every poll.
434            return Ok(report);
435        }
436
437        let haves = have_list(&local);
438        report.haves_sent = haves.len();
439
440        // At most two: round one negotiates, round two ends it. See the module
441        // doc for why there is no round three to send.
442        for round in 0..2 {
443            let done = round == 1;
444            self.ask_fetch(&capabilities, &wants, &haves, done, output)?;
445            output.flush()?;
446            let got_pack = self.read_fetch_response(input, ingest, &mut report)?;
447            report.rounds += 1;
448            if got_pack {
449                return Ok(report);
450            }
451            if done {
452                return Err(Error::protocol(
453                    "the remote answered a `done` fetch without a packfile section",
454                ));
455            }
456        }
457        unreachable!("the `done` round either returns a pack or errors above")
458    }
459
460    /// Which advertised references the plan takes, and what to `want`.
461    ///
462    /// A reference already at the advertised id yields no plan entry and no
463    /// `want`: it is the commonest outcome of a mirror poll, and asking for it
464    /// would cost the remote a walk for objects this store already has.
465    fn plan(
466        &self,
467        advertised: &[AdvertisedRef],
468        local: &[(String, ObjectId)],
469    ) -> (Vec<RefUpdatePlan>, Vec<ObjectId>) {
470        let mut updates = Vec::new();
471        let mut wants: Vec<ObjectId> = Vec::new();
472        for entry in advertised {
473            if !(self.wanted)(&entry.name) {
474                continue;
475            }
476            let Some(new) = entry.target else {
477                // An unborn reference. There is nothing to fetch and nothing to
478                // point anything at; a plan entry with no id would be an
479                // update the caller cannot apply.
480                continue;
481            };
482            let old = local
483                .iter()
484                .find(|(name, _)| *name == entry.name)
485                .map(|(_, id)| *id);
486            if old == Some(new) {
487                continue;
488            }
489            updates.push(RefUpdatePlan {
490                name: entry.name.clone(),
491                old,
492                new,
493            });
494            if !wants.contains(&new) {
495                wants.push(new);
496            }
497        }
498        (updates, wants)
499    }
500
501    /// Write one `command=fetch`.
502    fn ask_fetch(
503        &self,
504        remote: &PeerCapabilities,
505        wants: &[ObjectId],
506        haves: &[ObjectId],
507        done: bool,
508        output: &mut dyn Write,
509    ) -> Result<()> {
510        wire_io::text(output, b"command=fetch")?;
511        wire_io::text(output, format!("agent={}", self.agent).as_bytes())?;
512        wire_io::text(
513            output,
514            format!("object-format={}", caps::object_format_name(self.hash_kind)).as_bytes(),
515        )?;
516        wire_io::delim(output)?;
517
518        // `thin-pack` is deliberately NOT asked for. A thin pack names delta
519        // bases that are not in it, and completing one means reading them out
520        // of the local store — but `PackIngest::ingest` is handed a stream and
521        // nothing else, by design, because that is what makes a pushed pack and
522        // a fetched pack the same bytes. Asking for a thin pack would hand the
523        // ingest a pack it cannot close, and the failure would land in the
524        // storage engine rather than here. It costs wire bytes on a negotiated
525        // fetch (the server's own `FetchRequest::thin_pack` doc measures 3.98x
526        // on a divergent fixture) and it costs nothing on a mirror's first
527        // clone, where every base is in the transfer anyway.
528        //
529        // `ofs-delta` is safe and is asked for: an offset delta names its base
530        // by distance *inside the same pack*, so the pack stays self-contained.
531        if may_send(caps::FETCH_ARGUMENTS, "ofs-delta", remote) {
532            wire_io::text(output, b"ofs-delta")?;
533        }
534        // Band 2 is read and discarded, so asking the remote to spend bytes
535        // rendering it would be asking for work nobody consumes.
536        if may_send(caps::FETCH_ARGUMENTS, "no-progress", remote) {
537            wire_io::text(output, b"no-progress")?;
538        }
539        for want in wants {
540            wire_io::text(output, format!("want {want}").as_bytes())?;
541        }
542        for have in haves {
543            wire_io::text(output, format!("have {have}").as_bytes())?;
544        }
545        if done {
546            wire_io::text(output, b"done")?;
547        }
548        wire_io::flush(output)?;
549        Ok(())
550    }
551
552    /// Read one `command=fetch` response, streaming any packfile into `ingest`.
553    ///
554    /// `Ok(true)` when a packfile arrived and the fetch is over; `Ok(false)`
555    /// when the remote answered the negotiation and stopped.
556    fn read_fetch_response(
557        &self,
558        input: &mut dyn BufRead,
559        ingest: &dyn PackIngest,
560        report: &mut FetchReport,
561    ) -> Result<bool> {
562        let mut budget = Budget::new(Some(DEFAULT_REQUEST_LIMIT_BYTES));
563        let mut section = Section::None;
564        loop {
565            let Some(packet) = read_packet(input)? else {
566                return Err(Error::UnexpectedEof("a fetch response"));
567            };
568            match packet {
569                // The response is over. Either the negotiation continues, or
570                // — after a packfile section — this is the response's own end.
571                Packet::Flush => return Ok(false),
572                // A section boundary. What follows is another section header.
573                Packet::Delim => section = Section::None,
574                Packet::ResponseEnd => return Ok(false),
575                Packet::Data(data) => {
576                    budget.spend(data.len(), "a fetch response")?;
577                    let text = String::from_utf8_lossy(data.strip_suffix(b"\n").unwrap_or(&data))
578                        .into_owned();
579                    match Section::header(&text) {
580                        // A section header. `packfile` is the one that does not
581                        // return here: its content is not pkt-line text, it is
582                        // a sideband stream, so it is read by a different
583                        // reader entirely.
584                        Some(Section::Packfile) => {
585                            self.take_packfile(input, ingest, report)?;
586                            return Ok(true);
587                        }
588                        Some(next) => section = next,
589                        None => self.section_line(section, &text, report)?,
590                    }
591                }
592            }
593        }
594    }
595
596    /// One content line inside a section that is not `packfile`.
597    fn section_line(&self, section: Section, text: &str, report: &mut FetchReport) -> Result<()> {
598        match section {
599            Section::Acknowledgments => {
600                // `NAK` and `ready` are both answers, not errors: see
601                // `crate::negotiate`, which writes them. `ready` means the
602                // packfile follows in this same response after the delim-pkt,
603                // so nothing has to be recorded for it — the loop simply keeps
604                // reading and meets the `packfile` header next.
605                if let Some(hex) = text.strip_prefix("ACK ") {
606                    report.acked.push(parse_oid(hex.trim(), report.hash_kind)?);
607                } else if text != "NAK" && text != "ready" {
608                    return Err(Error::protocol(format!(
609                        "unexpected {text:?} in the acknowledgments section"
610                    )));
611                }
612                Ok(())
613            }
614            // Nothing here asks for a shallow fetch or a `want-ref`, so these
615            // sections cannot appear — but they are *tolerated* rather than
616            // refused, because a section this client did not request is the
617            // remote being generous, not the remote being wrong, and a mirror
618            // must not fail on it.
619            Section::ShallowInfo | Section::WantedRefs => Ok(()),
620            Section::Packfile => unreachable!("the packfile section is read as a stream"),
621            Section::None => Err(Error::protocol(format!(
622                "the remote sent {text:?} outside any section of a fetch response"
623            ))),
624        }
625    }
626
627    /// Stream the packfile into `ingest`, counting what passes.
628    fn take_packfile(
629        &self,
630        input: &mut dyn BufRead,
631        ingest: &dyn PackIngest,
632        report: &mut FetchReport,
633    ) -> Result<()> {
634        let mut sideband = Sideband::new(input);
635        let mut stream = PackStream::open(&mut sideband, self.progress)?;
636        report.objects_total = stream.objects_total;
637        if let Some(progress) = self.progress {
638            progress.objects_total(stream.objects_total);
639        }
640
641        let receipt = match &self.ceiling {
642            None => ingest.ingest(&mut stream)?,
643            // The receive loop's own guard, over the same stream. The cut is
644            // read off the guard and not recognised in whatever the ingest made
645            // of a truncated pack: "your namespace is full" and "the pack is
646            // corrupt" are not the same sentence to anybody.
647            Some(ceiling) => {
648                let mut counting =
649                    crate::receive_pack::Counting::new(&mut stream, Some(ceiling.clone()));
650                let received = ingest.ingest(&mut counting);
651                if counting.cut {
652                    report.pack_bytes = counting.bytes;
653                    return Err(Error::OverCeiling(ceiling.refusal.clone()));
654                }
655                received?
656            }
657        };
658        report.pack_bytes = stream.bytes;
659        // The ingest is entitled to stop at the pack's trailer and leave the
660        // section's flush-pkt unread; draining here is what turns a *cut*
661        // stream into an error rather than a fetch that quietly ends early.
662        stream.drain()?;
663        report.pack_bytes = stream.bytes;
664
665        let indexed = receipt.indexed.num_objects;
666        if indexed != report.objects_total {
667            return Err(Error::protocol(format!(
668                "the packfile header declared {} objects and the ingest indexed {indexed}; \
669                 a transfer that lost objects between the wire and the store must not read as \
670                 a completed fetch",
671                report.objects_total
672            )));
673        }
674        report.objects_done = indexed;
675        report.receipt = Some(receipt);
676        Ok(())
677    }
678}
679
680// ════════════════════════════════════════════════════════════════════════════
681// The conversation, in the order it happens
682// ════════════════════════════════════════════════════════════════════════════
683
684/// Read the remote's v2 capability advertisement and check who it is.
685///
686/// `version 2` must be the first line. A remote that opens with a *reference*
687/// advertisement is speaking v0, and the failure says so rather than reporting
688/// a malformed capability: that is the difference between "this remote is old"
689/// and "this remote is broken", and only one of them is worth a bug report.
690fn greet(hash_kind: Kind, input: &mut dyn BufRead) -> Result<PeerCapabilities> {
691    let capabilities = read_capabilities(input)?;
692
693    // Object-format negotiation, from the client's chair. `caps::check_object_format` is the
694    // server's half of the same check and cannot be reused verbatim: its error
695    // is worded for a server refusing a client, and an operator reading "the
696    // client asked for" about their own mirror learns nothing.
697    match capabilities.value("object-format") {
698        // A remote that does not say is git before 2.28, which is sha1 by
699        // definition. A sha256 client must not assume that silence means it.
700        None if hash_kind == Kind::Sha1 => {}
701        None => {
702            return Err(Error::RemoteObjectFormat {
703                ours: caps::object_format_name(hash_kind),
704                theirs: "unstated, which means sha1".to_owned(),
705            })
706        }
707        Some(name) if caps::object_format_from_name(name) == Some(hash_kind) => {}
708        Some(name) => {
709            return Err(Error::RemoteObjectFormat {
710                ours: caps::object_format_name(hash_kind),
711                theirs: name.to_owned(),
712            })
713        }
714    }
715    Ok(capabilities)
716}
717
718/// The v2 capability advertisement, read and parsed; no format is checked.
719fn read_capabilities(input: &mut dyn BufRead) -> Result<PeerCapabilities> {
720    let mut budget = Budget::new(Some(DEFAULT_REQUEST_LIMIT_BYTES));
721    let lines = wire_io::read_section(input, "a v2 capability advertisement", &mut budget)?;
722    let Some((first, rest)) = lines.split_first() else {
723        return Err(Error::protocol(
724            "the remote's advertisement was empty; a v2 server opens with `version 2`",
725        ));
726    };
727    let first = String::from_utf8_lossy(first).into_owned();
728    if first.trim() != "version 2" {
729        return Err(Error::protocol(format!(
730            "the remote opened with {first:?} and not `version 2`; this is a protocol-v2 client \
731             and that line is what a v0 reference advertisement looks like"
732        )));
733    }
734    let text: Vec<String> = rest
735        .iter()
736        .map(|line| String::from_utf8_lossy(line).into_owned())
737        .collect();
738    Ok(PeerCapabilities::parse(&text.join(" ")))
739}
740
741/// Write `command=ls-refs`.
742///
743/// **No `ref-prefix`.** The ref plan is an arbitrary `Fn(&str) -> bool`, and a
744/// predicate cannot be turned into a prefix list: any narrowing this module
745/// invented would either be wider than the plan (and pointless) or narrower
746/// (and would silently drop references the caller asked for, which is the
747/// failure a mirror can least afford to have). The filtering happens locally,
748/// against the whole advertisement, where it is the caller's own predicate that
749/// decides. A caller fetching one branch of a hundred-thousand-ref repository
750/// pays for the listing; that is a measurement away from being worth a
751/// `ref-prefix` argument on [`FetchPack`], and nothing here would have to move
752/// to add one.
753fn ask_ls_refs(
754    hash_kind: Kind,
755    agent: &str,
756    remote: &PeerCapabilities,
757    output: &mut dyn Write,
758) -> Result<()> {
759    wire_io::text(output, b"command=ls-refs")?;
760    wire_io::text(output, format!("agent={agent}").as_bytes())?;
761    wire_io::text(
762        output,
763        format!("object-format={}", caps::object_format_name(hash_kind)).as_bytes(),
764    )?;
765    wire_io::delim(output)?;
766    // `symrefs` so a caller can see what `HEAD` points at, and `peel` so an
767    // annotated tag arrives with the commit it peels to. Both are ungated in
768    // `LS_REFS_ARGUMENTS`; `unborn` is gated, and is asked for only if the
769    // remote advertised it — the same table the server parses against decides,
770    // so this client cannot send an argument that server would refuse.
771    wire_io::text(output, b"symrefs")?;
772    wire_io::text(output, b"peel")?;
773    if may_send(caps::LS_REFS_ARGUMENTS, "unborn", remote) {
774        wire_io::text(output, b"unborn")?;
775    }
776    wire_io::flush(output)?;
777    Ok(())
778}
779
780/// Read the `ls-refs` answer.
781fn read_ref_listing(input: &mut dyn BufRead) -> Result<Vec<AdvertisedRef>> {
782    let mut budget = Budget::new(Some(DEFAULT_REQUEST_LIMIT_BYTES));
783    let lines = wire_io::read_section(input, "an ls-refs response", &mut budget)?;
784    let mut out = Vec::with_capacity(lines.len());
785    for line in lines {
786        out.push(parse_ref_line(&String::from_utf8_lossy(&line))?);
787    }
788    Ok(out)
789}
790
791/// `<oid> <name> [symref-target:<name>] [peeled:<oid>]`, or `unborn <name> …`.
792///
793/// The hash kind is **not** passed in and the oid is not parsed against it:
794/// `ObjectId::from_hex` decides the kind from the length, and the length is what
795/// the remote actually sent. A parse that forced the caller's kind would turn a
796/// remote sending the wrong width into a *truncation* instead of an error, which
797/// is the same class of silent-wrong-answer as object-format negotiation itself. The kinds are
798/// reconciled once, in [`greet`], by name.
799fn parse_ref_line(line: &str) -> Result<AdvertisedRef> {
800    let mut fields = line.split(' ');
801    let first = fields
802        .next()
803        .ok_or_else(|| Error::protocol("an empty line in an ls-refs response"))?;
804    let name = fields
805        .next()
806        .ok_or_else(|| Error::protocol(format!("{line:?} names no reference")))?
807        .to_owned();
808    let target = if first == "unborn" {
809        None
810    } else {
811        Some(parse_any_oid(first)?)
812    };
813
814    let mut peeled = None;
815    let mut symref_target = None;
816    for attribute in fields {
817        if let Some(value) = attribute.strip_prefix("peeled:") {
818            peeled = Some(parse_any_oid(value)?);
819        } else if let Some(value) = attribute.strip_prefix("symref-target:") {
820            symref_target = Some(value.to_owned());
821        }
822        // Anything else is an attribute a later git added. Ignored on purpose:
823        // an unknown *attribute* on a reference is not a protocol violation, and
824        // refusing one would make this client fail against a newer remote for a
825        // field it does not read.
826    }
827    Ok(AdvertisedRef {
828        name,
829        target,
830        peeled,
831        symref_target,
832    })
833}
834
835/// The `have` list: every local tip, deduplicated, in the order given.
836///
837/// **Every tip, not a walk back through history.** git's client walks because it
838/// is looking for a near cut point without sending its whole ref list; a mirror
839/// already holds the complete offer in its refs, and sending it in one round is
840/// both correct and one round shorter. The server's negotiation is bounded by
841/// the oldest `have`'s commit date either way (`crate::negotiate`).
842fn have_list(local: &[(String, ObjectId)]) -> Vec<ObjectId> {
843    let mut out: Vec<ObjectId> = Vec::with_capacity(local.len());
844    for (_, id) in local {
845        if !out.contains(id) {
846            out.push(*id);
847        }
848    }
849    out
850}
851
852/// May this client send `name`, given what the remote advertised?
853///
854/// Reads [`caps::FETCH_ARGUMENTS`] / [`caps::LS_REFS_ARGUMENTS`] — the **same**
855/// tables `FetchRequest::parse` and `LsRefsRequest::parse` reject unknown
856/// arguments against. An ungated argument is always allowed; a gated one needs
857/// its feature in the remote's advertised capability value. Sending a gated
858/// argument to a remote that did not advertise its feature is exactly the error
859/// gunnar's own server answers with `UnknownArgument`, so a client that guessed
860/// would break a fetch against gunnar itself.
861fn may_send(table: &'static [Argument], name: &'static str, remote: &PeerCapabilities) -> bool {
862    let Some(argument) = caps::argument(table, name) else {
863        return false;
864    };
865    let Some(feature) = argument.feature else {
866        return true;
867    };
868    let capability = match table.as_ptr() == caps::FETCH_ARGUMENTS.as_ptr() {
869        true => "fetch",
870        false => "ls-refs",
871    };
872    remote
873        .value(capability)
874        .is_some_and(|value| value.split(' ').any(|offered| offered == feature))
875}
876
877fn parse_oid(hex: &str, hash_kind: Kind) -> Result<ObjectId> {
878    let id = parse_any_oid(hex)?;
879    if id.kind() != hash_kind {
880        return Err(Error::RemoteObjectFormat {
881            ours: caps::object_format_name(hash_kind),
882            theirs: caps::object_format_name(id.kind()).to_owned(),
883        });
884    }
885    Ok(id)
886}
887
888fn parse_any_oid(hex: &str) -> Result<ObjectId> {
889    ObjectId::from_hex(hex.as_bytes())
890        .map_err(|err| Error::protocol(format!("{hex:?} is not an object id: {err}")))
891}
892
893/// Which section of a fetch response is being read.
894#[derive(Debug, Clone, Copy, PartialEq, Eq)]
895enum Section {
896    None,
897    Acknowledgments,
898    ShallowInfo,
899    WantedRefs,
900    Packfile,
901}
902
903impl Section {
904    /// Is this line a section header rather than a section's content?
905    fn header(text: &str) -> Option<Section> {
906        match text {
907            "acknowledgments" => Some(Section::Acknowledgments),
908            "shallow-info" => Some(Section::ShallowInfo),
909            "wanted-refs" => Some(Section::WantedRefs),
910            "packfile" => Some(Section::Packfile),
911            _ => None,
912        }
913    }
914}
915
916// ════════════════════════════════════════════════════════════════════════════
917// The packfile: demultiplexed, counted, never held
918// ════════════════════════════════════════════════════════════════════════════
919
920/// The client half of [`crate::wire_io::SidebandWriter`]: a `BufRead` over the
921/// payload of sideband 1.
922///
923/// It holds **one pkt-line**. That is the whole streaming guarantee: whatever
924/// the size of the packfile, the most this side of the wire ever has in memory
925/// is one 65 515-byte sideband payload, and the ingest is driven by the pipe
926/// rather than by a buffer that filled up first.
927///
928/// Band 2 is progress and is dropped; band 3 is the remote reporting a fatal
929/// condition, and it becomes [`Error::Remote`] rather than a short read — a
930/// server that says *"repository not found"* on band 3 and closes must not reach
931/// the ingest as a truncated pack.
932struct Sideband<'a> {
933    input: &'a mut dyn BufRead,
934    line: Vec<u8>,
935    pos: usize,
936    /// The section's flush-pkt has been read; there is nothing more.
937    finished: bool,
938}
939
940impl<'a> Sideband<'a> {
941    fn new(input: &'a mut dyn BufRead) -> Self {
942        Sideband {
943            input,
944            line: Vec::new(),
945            pos: 0,
946            finished: false,
947        }
948    }
949}
950
951impl BufRead for Sideband<'_> {
952    fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
953        while self.pos >= self.line.len() && !self.finished {
954            self.line.clear();
955            self.pos = 0;
956            match read_packet(self.input).map_err(as_io)? {
957                // Not a clean end: the packfile section is flush-terminated, so
958                // a hang-up here is a cut transfer and nothing else.
959                None => {
960                    return Err(std::io::Error::new(
961                        std::io::ErrorKind::UnexpectedEof,
962                        "the remote hung up inside the packfile section",
963                    ))
964                }
965                Some(Packet::Flush) => self.finished = true,
966                Some(Packet::Data(data)) => {
967                    let Some((band, payload)) = data.split_first() else {
968                        // A zero-length data packet carries no band byte. Legal
969                        // framing, meaningless here, and skipping it is what
970                        // keeps the loop from treating it as end of stream.
971                        continue;
972                    };
973                    match *band {
974                        BAND_PACK => self.line.extend_from_slice(payload),
975                        BAND_PROGRESS => {}
976                        BAND_ERROR => {
977                            return Err(as_io(Error::Remote(
978                                String::from_utf8_lossy(payload).trim().to_owned(),
979                            )))
980                        }
981                        other => {
982                            return Err(as_io(Error::protocol(format!(
983                                "the remote used sideband {other}, which the protocol does not \
984                                 define"
985                            ))))
986                        }
987                    }
988                }
989                Some(other) => {
990                    return Err(as_io(Error::protocol(format!(
991                        "unexpected {other:?} inside the packfile section"
992                    ))))
993                }
994            }
995        }
996        Ok(&self.line[self.pos..])
997    }
998
999    fn consume(&mut self, amount: usize) {
1000        self.pos = (self.pos + amount).min(self.line.len());
1001    }
1002}
1003
1004impl std::io::Read for Sideband<'_> {
1005    fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
1006        let available = self.fill_buf()?;
1007        let take = available.len().min(out.len());
1008        out[..take].copy_from_slice(&available[..take]);
1009        self.consume(take);
1010        Ok(take)
1011    }
1012}
1013
1014/// The demultiplexed packfile, with its header already read.
1015///
1016/// # Why the header is read here and handed on again
1017///
1018/// Byte 0..4 is `PACK`, 4..8 is the version, and **8..12 is the object count**.
1019/// That count is the denominator of every progress bar this will ever sit
1020/// behind, and it is on the wire before the first object is. Reading it here
1021/// costs twelve bytes and makes it available at the *start* of the transfer;
1022/// asking the ingest for it makes it available at the end, which is the one
1023/// moment nobody needs a percentage.
1024///
1025/// The twelve bytes are then re-offered to the ingest verbatim, so what
1026/// [`PackIngest::ingest`] sees is a whole packfile positioned on the `P` of
1027/// `PACK`, exactly as `receive_pack` positions a pushed one. The ingest is not
1028/// told that anything read it first.
1029struct PackStream<'a, 'b> {
1030    inner: &'a mut Sideband<'b>,
1031    /// The header, until the ingest has taken it.
1032    header: [u8; 12],
1033    header_pos: usize,
1034    /// What byte 8..12 declared.
1035    objects_total: u32,
1036    /// Packfile bytes handed on so far.
1037    bytes: u64,
1038    progress: Option<&'a dyn FetchProgress>,
1039}
1040
1041impl<'a, 'b> PackStream<'a, 'b> {
1042    fn open(inner: &'a mut Sideband<'b>, progress: Option<&'a dyn FetchProgress>) -> Result<Self> {
1043        let mut header = [0u8; 12];
1044        read_exact(inner, &mut header)?;
1045        if &header[..4] != b"PACK" {
1046            return Err(Error::protocol(format!(
1047                "the packfile section began with {:?} and not `PACK`",
1048                String::from_utf8_lossy(&header[..4])
1049            )));
1050        }
1051        let version = u32::from_be_bytes([header[4], header[5], header[6], header[7]]);
1052        if version != 2 && version != 3 {
1053            return Err(Error::protocol(format!(
1054                "the remote sent a version-{version} packfile; git defines 2 and 3"
1055            )));
1056        }
1057        let objects_total = u32::from_be_bytes([header[8], header[9], header[10], header[11]]);
1058        Ok(PackStream {
1059            inner,
1060            header,
1061            header_pos: 0,
1062            objects_total,
1063            bytes: 0,
1064            progress,
1065        })
1066    }
1067
1068    /// Read whatever the ingest left, up to the section's flush-pkt.
1069    ///
1070    /// An ingest stops at the packfile's trailer, which is where the pack ends
1071    /// and not where the *section* ends. Reaching the flush here is what proves
1072    /// the remote finished sending; without it a stream cut after the trailer's
1073    /// last byte would be indistinguishable from a complete one.
1074    fn drain(&mut self) -> Result<()> {
1075        loop {
1076            let taken = {
1077                let rest = self.fill_buf()?;
1078                if rest.is_empty() {
1079                    return Ok(());
1080                }
1081                rest.len()
1082            };
1083            self.consume(taken);
1084        }
1085    }
1086}
1087
1088impl BufRead for PackStream<'_, '_> {
1089    fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
1090        if self.header_pos < self.header.len() {
1091            return Ok(&self.header[self.header_pos..]);
1092        }
1093        self.inner.fill_buf()
1094    }
1095
1096    fn consume(&mut self, amount: usize) {
1097        let from_header = amount.min(self.header.len() - self.header_pos);
1098        self.header_pos += from_header;
1099        if amount > from_header {
1100            self.inner.consume(amount - from_header);
1101        }
1102        self.bytes += amount as u64;
1103        if let Some(progress) = self.progress {
1104            progress.pack_bytes(self.bytes);
1105        }
1106    }
1107}
1108
1109impl std::io::Read for PackStream<'_, '_> {
1110    fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
1111        let take = {
1112            let available = self.fill_buf()?;
1113            let take = available.len().min(out.len());
1114            out[..take].copy_from_slice(&available[..take]);
1115            take
1116        };
1117        self.consume(take);
1118        Ok(take)
1119    }
1120}
1121
1122/// Fill `buf` completely, refusing a short read.
1123///
1124/// `Read::read_exact` would do, and its error would say `failed to fill whole
1125/// buffer` — which for a packfile header is the least useful twelve words in
1126/// the transfer. This one names what was being read.
1127fn read_exact(input: &mut dyn BufRead, buf: &mut [u8]) -> Result<()> {
1128    let mut filled = 0;
1129    while filled < buf.len() {
1130        let read = std::io::Read::read(input, &mut buf[filled..])?;
1131        if read == 0 {
1132            return Err(Error::UnexpectedEof("a packfile header"));
1133        }
1134        filled += read;
1135    }
1136    Ok(())
1137}
1138
1139/// Carry a `gunnar-wire` error through a `BufRead` signature and back out.
1140///
1141/// `Error::Io(io::Error)` already exists in both directions, so a refusal made
1142/// inside `fill_buf` — a band-3 message, an unknown sideband — reaches the
1143/// caller with its text intact rather than as a bare "other error".
1144fn as_io(err: Error) -> std::io::Error {
1145    match err {
1146        Error::Io(err) => err,
1147        other => std::io::Error::other(other.to_string()),
1148    }
1149}