use std::io::{BufRead, Write};
use gix_hash::{Kind, ObjectId};
use gunnar_pktline::{Packet, BAND_ERROR, BAND_PACK, BAND_PROGRESS};
use crate::caps::{self, Argument, PeerCapabilities};
use crate::limits::{Budget, DEFAULT_REQUEST_LIMIT_BYTES};
use crate::receive_pack::{PackIngest, PackReceipt};
use crate::upload_pack::AdvertisedRef;
use crate::wire_io::{self, read_packet};
use crate::{Error, Result};
#[derive(Debug, Clone)]
pub struct Advertisement {
pub hash_kind: Kind,
pub capabilities: PeerCapabilities,
pub refs: Vec<AdvertisedRef>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefUpdatePlan {
pub name: String,
pub old: Option<ObjectId>,
pub new: ObjectId,
}
#[derive(Debug, Clone)]
pub struct FetchReport {
pub hash_kind: Kind,
pub advertised: Vec<AdvertisedRef>,
pub updates: Vec<RefUpdatePlan>,
pub receipt: Option<PackReceipt>,
pub objects_total: u32,
pub objects_done: u32,
pub pack_bytes: u64,
pub haves_sent: usize,
pub acked: Vec<ObjectId>,
pub rounds: usize,
}
pub trait FetchProgress {
fn objects_total(&self, objects: u32);
fn pack_bytes(&self, streamed: u64);
}
pub trait LocalRefs {
fn tips(&self) -> Result<Vec<(String, ObjectId)>>;
}
impl LocalRefs for Vec<(String, ObjectId)> {
fn tips(&self) -> Result<Vec<(String, ObjectId)>> {
Ok(self.clone())
}
}
pub fn mirror(name: &str) -> bool {
name.starts_with("refs/heads/") || name.starts_with("refs/tags/")
}
pub fn ls_remote(
hash_kind: Kind,
input: &mut dyn BufRead,
output: &mut dyn Write,
) -> Result<Advertisement> {
let capabilities = greet(hash_kind, input)?;
let agent = caps::default_agent();
ask_ls_refs(hash_kind, &agent, &capabilities, output)?;
output.flush()?;
let refs = read_ref_listing(input)?;
Ok(Advertisement {
hash_kind,
capabilities,
refs,
})
}
pub fn ls_remote_any(input: &mut dyn BufRead, output: &mut dyn Write) -> Result<Advertisement> {
let capabilities = read_capabilities(input)?;
let hash_kind = match capabilities.value("object-format") {
None => Kind::Sha1,
Some(name) => {
caps::object_format_from_name(name).ok_or_else(|| Error::RemoteObjectFormat {
ours: "sha1 or sha256",
theirs: name.to_owned(),
})?
}
};
let agent = caps::default_agent();
ask_ls_refs(hash_kind, &agent, &capabilities, output)?;
output.flush()?;
let refs = read_ref_listing(input)?;
Ok(Advertisement {
hash_kind,
capabilities,
refs,
})
}
pub struct FetchPack<'a> {
hash_kind: Kind,
agent: String,
wanted: &'a dyn Fn(&str) -> bool,
local: &'a dyn LocalRefs,
progress: Option<&'a dyn FetchProgress>,
ceiling: Option<crate::receive_pack::PackCeiling>,
}
impl<'a> FetchPack<'a> {
pub fn new(
hash_kind: Kind,
wanted: &'a dyn Fn(&str) -> bool,
local: &'a dyn LocalRefs,
) -> Self {
FetchPack {
hash_kind,
agent: caps::default_agent(),
wanted,
local,
progress: None,
ceiling: None,
}
}
pub fn with_ceiling(mut self, ceiling: crate::receive_pack::PackCeiling) -> Self {
self.ceiling = Some(ceiling);
self
}
pub fn with_agent(mut self, agent: String) -> Self {
self.agent = agent;
self
}
pub fn with_progress(mut self, progress: &'a dyn FetchProgress) -> Self {
self.progress = Some(progress);
self
}
pub fn hash_kind(&self) -> Kind {
self.hash_kind
}
pub fn fetch(
&self,
ingest: &dyn PackIngest,
input: &mut dyn BufRead,
output: &mut dyn Write,
) -> Result<FetchReport> {
let capabilities = greet(self.hash_kind, input)?;
ask_ls_refs(self.hash_kind, &self.agent, &capabilities, output)?;
output.flush()?;
let advertised = read_ref_listing(input)?;
let local = self.local.tips()?;
let (updates, wants) = self.plan(&advertised, &local);
let mut report = FetchReport {
hash_kind: self.hash_kind,
advertised,
updates,
receipt: None,
objects_total: 0,
objects_done: 0,
pack_bytes: 0,
haves_sent: 0,
acked: Vec::new(),
rounds: 0,
};
if wants.is_empty() {
return Ok(report);
}
let haves = have_list(&local);
report.haves_sent = haves.len();
for round in 0..2 {
let done = round == 1;
self.ask_fetch(&capabilities, &wants, &haves, done, output)?;
output.flush()?;
let got_pack = self.read_fetch_response(input, ingest, &mut report)?;
report.rounds += 1;
if got_pack {
return Ok(report);
}
if done {
return Err(Error::protocol(
"the remote answered a `done` fetch without a packfile section",
));
}
}
unreachable!("the `done` round either returns a pack or errors above")
}
fn plan(
&self,
advertised: &[AdvertisedRef],
local: &[(String, ObjectId)],
) -> (Vec<RefUpdatePlan>, Vec<ObjectId>) {
let mut updates = Vec::new();
let mut wants: Vec<ObjectId> = Vec::new();
for entry in advertised {
if !(self.wanted)(&entry.name) {
continue;
}
let Some(new) = entry.target else {
continue;
};
let old = local
.iter()
.find(|(name, _)| *name == entry.name)
.map(|(_, id)| *id);
if old == Some(new) {
continue;
}
updates.push(RefUpdatePlan {
name: entry.name.clone(),
old,
new,
});
if !wants.contains(&new) {
wants.push(new);
}
}
(updates, wants)
}
fn ask_fetch(
&self,
remote: &PeerCapabilities,
wants: &[ObjectId],
haves: &[ObjectId],
done: bool,
output: &mut dyn Write,
) -> Result<()> {
wire_io::text(output, b"command=fetch")?;
wire_io::text(output, format!("agent={}", self.agent).as_bytes())?;
wire_io::text(
output,
format!("object-format={}", caps::object_format_name(self.hash_kind)).as_bytes(),
)?;
wire_io::delim(output)?;
if may_send(caps::FETCH_ARGUMENTS, "ofs-delta", remote) {
wire_io::text(output, b"ofs-delta")?;
}
if may_send(caps::FETCH_ARGUMENTS, "no-progress", remote) {
wire_io::text(output, b"no-progress")?;
}
for want in wants {
wire_io::text(output, format!("want {want}").as_bytes())?;
}
for have in haves {
wire_io::text(output, format!("have {have}").as_bytes())?;
}
if done {
wire_io::text(output, b"done")?;
}
wire_io::flush(output)?;
Ok(())
}
fn read_fetch_response(
&self,
input: &mut dyn BufRead,
ingest: &dyn PackIngest,
report: &mut FetchReport,
) -> Result<bool> {
let mut budget = Budget::new(Some(DEFAULT_REQUEST_LIMIT_BYTES));
let mut section = Section::None;
loop {
let Some(packet) = read_packet(input)? else {
return Err(Error::UnexpectedEof("a fetch response"));
};
match packet {
Packet::Flush => return Ok(false),
Packet::Delim => section = Section::None,
Packet::ResponseEnd => return Ok(false),
Packet::Data(data) => {
budget.spend(data.len(), "a fetch response")?;
let text = String::from_utf8_lossy(data.strip_suffix(b"\n").unwrap_or(&data))
.into_owned();
match Section::header(&text) {
Some(Section::Packfile) => {
self.take_packfile(input, ingest, report)?;
return Ok(true);
}
Some(next) => section = next,
None => self.section_line(section, &text, report)?,
}
}
}
}
}
fn section_line(&self, section: Section, text: &str, report: &mut FetchReport) -> Result<()> {
match section {
Section::Acknowledgments => {
if let Some(hex) = text.strip_prefix("ACK ") {
report.acked.push(parse_oid(hex.trim(), report.hash_kind)?);
} else if text != "NAK" && text != "ready" {
return Err(Error::protocol(format!(
"unexpected {text:?} in the acknowledgments section"
)));
}
Ok(())
}
Section::ShallowInfo | Section::WantedRefs => Ok(()),
Section::Packfile => unreachable!("the packfile section is read as a stream"),
Section::None => Err(Error::protocol(format!(
"the remote sent {text:?} outside any section of a fetch response"
))),
}
}
fn take_packfile(
&self,
input: &mut dyn BufRead,
ingest: &dyn PackIngest,
report: &mut FetchReport,
) -> Result<()> {
let mut sideband = Sideband::new(input);
let mut stream = PackStream::open(&mut sideband, self.progress)?;
report.objects_total = stream.objects_total;
if let Some(progress) = self.progress {
progress.objects_total(stream.objects_total);
}
let receipt = match &self.ceiling {
None => ingest.ingest(&mut stream)?,
Some(ceiling) => {
let mut counting =
crate::receive_pack::Counting::new(&mut stream, Some(ceiling.clone()));
let received = ingest.ingest(&mut counting);
if counting.cut {
report.pack_bytes = counting.bytes;
return Err(Error::OverCeiling(ceiling.refusal.clone()));
}
received?
}
};
report.pack_bytes = stream.bytes;
stream.drain()?;
report.pack_bytes = stream.bytes;
let indexed = receipt.indexed.num_objects;
if indexed != report.objects_total {
return Err(Error::protocol(format!(
"the packfile header declared {} objects and the ingest indexed {indexed}; \
a transfer that lost objects between the wire and the store must not read as \
a completed fetch",
report.objects_total
)));
}
report.objects_done = indexed;
report.receipt = Some(receipt);
Ok(())
}
}
fn greet(hash_kind: Kind, input: &mut dyn BufRead) -> Result<PeerCapabilities> {
let capabilities = read_capabilities(input)?;
match capabilities.value("object-format") {
None if hash_kind == Kind::Sha1 => {}
None => {
return Err(Error::RemoteObjectFormat {
ours: caps::object_format_name(hash_kind),
theirs: "unstated, which means sha1".to_owned(),
})
}
Some(name) if caps::object_format_from_name(name) == Some(hash_kind) => {}
Some(name) => {
return Err(Error::RemoteObjectFormat {
ours: caps::object_format_name(hash_kind),
theirs: name.to_owned(),
})
}
}
Ok(capabilities)
}
fn read_capabilities(input: &mut dyn BufRead) -> Result<PeerCapabilities> {
let mut budget = Budget::new(Some(DEFAULT_REQUEST_LIMIT_BYTES));
let lines = wire_io::read_section(input, "a v2 capability advertisement", &mut budget)?;
let Some((first, rest)) = lines.split_first() else {
return Err(Error::protocol(
"the remote's advertisement was empty; a v2 server opens with `version 2`",
));
};
let first = String::from_utf8_lossy(first).into_owned();
if first.trim() != "version 2" {
return Err(Error::protocol(format!(
"the remote opened with {first:?} and not `version 2`; this is a protocol-v2 client \
and that line is what a v0 reference advertisement looks like"
)));
}
let text: Vec<String> = rest
.iter()
.map(|line| String::from_utf8_lossy(line).into_owned())
.collect();
Ok(PeerCapabilities::parse(&text.join(" ")))
}
fn ask_ls_refs(
hash_kind: Kind,
agent: &str,
remote: &PeerCapabilities,
output: &mut dyn Write,
) -> Result<()> {
wire_io::text(output, b"command=ls-refs")?;
wire_io::text(output, format!("agent={agent}").as_bytes())?;
wire_io::text(
output,
format!("object-format={}", caps::object_format_name(hash_kind)).as_bytes(),
)?;
wire_io::delim(output)?;
wire_io::text(output, b"symrefs")?;
wire_io::text(output, b"peel")?;
if may_send(caps::LS_REFS_ARGUMENTS, "unborn", remote) {
wire_io::text(output, b"unborn")?;
}
wire_io::flush(output)?;
Ok(())
}
fn read_ref_listing(input: &mut dyn BufRead) -> Result<Vec<AdvertisedRef>> {
let mut budget = Budget::new(Some(DEFAULT_REQUEST_LIMIT_BYTES));
let lines = wire_io::read_section(input, "an ls-refs response", &mut budget)?;
let mut out = Vec::with_capacity(lines.len());
for line in lines {
out.push(parse_ref_line(&String::from_utf8_lossy(&line))?);
}
Ok(out)
}
fn parse_ref_line(line: &str) -> Result<AdvertisedRef> {
let mut fields = line.split(' ');
let first = fields
.next()
.ok_or_else(|| Error::protocol("an empty line in an ls-refs response"))?;
let name = fields
.next()
.ok_or_else(|| Error::protocol(format!("{line:?} names no reference")))?
.to_owned();
let target = if first == "unborn" {
None
} else {
Some(parse_any_oid(first)?)
};
let mut peeled = None;
let mut symref_target = None;
for attribute in fields {
if let Some(value) = attribute.strip_prefix("peeled:") {
peeled = Some(parse_any_oid(value)?);
} else if let Some(value) = attribute.strip_prefix("symref-target:") {
symref_target = Some(value.to_owned());
}
}
Ok(AdvertisedRef {
name,
target,
peeled,
symref_target,
})
}
fn have_list(local: &[(String, ObjectId)]) -> Vec<ObjectId> {
let mut out: Vec<ObjectId> = Vec::with_capacity(local.len());
for (_, id) in local {
if !out.contains(id) {
out.push(*id);
}
}
out
}
fn may_send(table: &'static [Argument], name: &'static str, remote: &PeerCapabilities) -> bool {
let Some(argument) = caps::argument(table, name) else {
return false;
};
let Some(feature) = argument.feature else {
return true;
};
let capability = match table.as_ptr() == caps::FETCH_ARGUMENTS.as_ptr() {
true => "fetch",
false => "ls-refs",
};
remote
.value(capability)
.is_some_and(|value| value.split(' ').any(|offered| offered == feature))
}
fn parse_oid(hex: &str, hash_kind: Kind) -> Result<ObjectId> {
let id = parse_any_oid(hex)?;
if id.kind() != hash_kind {
return Err(Error::RemoteObjectFormat {
ours: caps::object_format_name(hash_kind),
theirs: caps::object_format_name(id.kind()).to_owned(),
});
}
Ok(id)
}
fn parse_any_oid(hex: &str) -> Result<ObjectId> {
ObjectId::from_hex(hex.as_bytes())
.map_err(|err| Error::protocol(format!("{hex:?} is not an object id: {err}")))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Section {
None,
Acknowledgments,
ShallowInfo,
WantedRefs,
Packfile,
}
impl Section {
fn header(text: &str) -> Option<Section> {
match text {
"acknowledgments" => Some(Section::Acknowledgments),
"shallow-info" => Some(Section::ShallowInfo),
"wanted-refs" => Some(Section::WantedRefs),
"packfile" => Some(Section::Packfile),
_ => None,
}
}
}
struct Sideband<'a> {
input: &'a mut dyn BufRead,
line: Vec<u8>,
pos: usize,
finished: bool,
}
impl<'a> Sideband<'a> {
fn new(input: &'a mut dyn BufRead) -> Self {
Sideband {
input,
line: Vec::new(),
pos: 0,
finished: false,
}
}
}
impl BufRead for Sideband<'_> {
fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
while self.pos >= self.line.len() && !self.finished {
self.line.clear();
self.pos = 0;
match read_packet(self.input).map_err(as_io)? {
None => {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"the remote hung up inside the packfile section",
))
}
Some(Packet::Flush) => self.finished = true,
Some(Packet::Data(data)) => {
let Some((band, payload)) = data.split_first() else {
continue;
};
match *band {
BAND_PACK => self.line.extend_from_slice(payload),
BAND_PROGRESS => {}
BAND_ERROR => {
return Err(as_io(Error::Remote(
String::from_utf8_lossy(payload).trim().to_owned(),
)))
}
other => {
return Err(as_io(Error::protocol(format!(
"the remote used sideband {other}, which the protocol does not \
define"
))))
}
}
}
Some(other) => {
return Err(as_io(Error::protocol(format!(
"unexpected {other:?} inside the packfile section"
))))
}
}
}
Ok(&self.line[self.pos..])
}
fn consume(&mut self, amount: usize) {
self.pos = (self.pos + amount).min(self.line.len());
}
}
impl std::io::Read for Sideband<'_> {
fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
let available = self.fill_buf()?;
let take = available.len().min(out.len());
out[..take].copy_from_slice(&available[..take]);
self.consume(take);
Ok(take)
}
}
struct PackStream<'a, 'b> {
inner: &'a mut Sideband<'b>,
header: [u8; 12],
header_pos: usize,
objects_total: u32,
bytes: u64,
progress: Option<&'a dyn FetchProgress>,
}
impl<'a, 'b> PackStream<'a, 'b> {
fn open(inner: &'a mut Sideband<'b>, progress: Option<&'a dyn FetchProgress>) -> Result<Self> {
let mut header = [0u8; 12];
read_exact(inner, &mut header)?;
if &header[..4] != b"PACK" {
return Err(Error::protocol(format!(
"the packfile section began with {:?} and not `PACK`",
String::from_utf8_lossy(&header[..4])
)));
}
let version = u32::from_be_bytes([header[4], header[5], header[6], header[7]]);
if version != 2 && version != 3 {
return Err(Error::protocol(format!(
"the remote sent a version-{version} packfile; git defines 2 and 3"
)));
}
let objects_total = u32::from_be_bytes([header[8], header[9], header[10], header[11]]);
Ok(PackStream {
inner,
header,
header_pos: 0,
objects_total,
bytes: 0,
progress,
})
}
fn drain(&mut self) -> Result<()> {
loop {
let taken = {
let rest = self.fill_buf()?;
if rest.is_empty() {
return Ok(());
}
rest.len()
};
self.consume(taken);
}
}
}
impl BufRead for PackStream<'_, '_> {
fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
if self.header_pos < self.header.len() {
return Ok(&self.header[self.header_pos..]);
}
self.inner.fill_buf()
}
fn consume(&mut self, amount: usize) {
let from_header = amount.min(self.header.len() - self.header_pos);
self.header_pos += from_header;
if amount > from_header {
self.inner.consume(amount - from_header);
}
self.bytes += amount as u64;
if let Some(progress) = self.progress {
progress.pack_bytes(self.bytes);
}
}
}
impl std::io::Read for PackStream<'_, '_> {
fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
let take = {
let available = self.fill_buf()?;
let take = available.len().min(out.len());
out[..take].copy_from_slice(&available[..take]);
take
};
self.consume(take);
Ok(take)
}
}
fn read_exact(input: &mut dyn BufRead, buf: &mut [u8]) -> Result<()> {
let mut filled = 0;
while filled < buf.len() {
let read = std::io::Read::read(input, &mut buf[filled..])?;
if read == 0 {
return Err(Error::UnexpectedEof("a packfile header"));
}
filled += read;
}
Ok(())
}
fn as_io(err: Error) -> std::io::Error {
match err {
Error::Io(err) => err,
other => std::io::Error::other(other.to_string()),
}
}