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}