use std::num::NonZeroU32;
use std::sync::Arc;
use super::{Advertisement, DownloadedFetch, FetchError, FetchLimits, KnownHistory, protocol};
use crate::ObjectId;
use crate::packet::Wire;
use crate::transport::TransportControl;
use crate::transport::http::{HttpRemote, RequestBody};
pub async fn receive_http(
remote: &HttpRemote,
select: impl FnOnce(&Advertisement) -> Vec<ObjectId>,
known: Option<Arc<KnownHistory>>,
limits: FetchLimits,
control: TransportControl<'_>,
) -> Result<DownloadedFetch, FetchError> {
receive_http_with_depth(remote, select, known, None, limits, control).await
}
pub async fn receive_http_with_depth(
remote: &HttpRemote,
select: impl FnOnce(&Advertisement) -> Vec<ObjectId>,
known: Option<Arc<KnownHistory>>,
depth: Option<NonZeroU32>,
limits: FetchLimits,
control: TransportControl<'_>,
) -> Result<DownloadedFetch, FetchError> {
receive_http_with_depth_and_progress(remote, select, known, depth, limits, control, |_| {})
.await
}
pub async fn receive_http_with_progress(
remote: &HttpRemote,
select: impl FnOnce(&Advertisement) -> Vec<ObjectId>,
known: Option<Arc<KnownHistory>>,
limits: FetchLimits,
control: TransportControl<'_>,
progress: impl FnMut(&[u8]) + Send,
) -> Result<DownloadedFetch, FetchError> {
receive_http_with_depth_and_progress(remote, select, known, None, limits, control, progress)
.await
}
pub async fn receive_http_with_depth_and_progress(
remote: &HttpRemote,
select: impl FnOnce(&Advertisement) -> Vec<ObjectId>,
known: Option<Arc<KnownHistory>>,
depth: Option<NonZeroU32>,
limits: FetchLimits,
control: TransportControl<'_>,
mut progress: impl FnMut(&[u8]) + Send,
) -> Result<DownloadedFetch, FetchError> {
#[cfg(feature = "tracing")]
let span = tracing::debug_span!(
target: "girt",
"fetch.http",
outcome = "incomplete",
failure_class = tracing::field::Empty,
effects = tracing::field::Empty,
wire_bytes = tracing::field::Empty,
);
let operation = async {
control.check()?;
let empty = KnownHistory::default();
let history = known.as_deref().unwrap_or(&empty);
protocol::validate_known(history, limits, control.cancel)?;
let (bytes, advertisement_bytes) = remote
.discover(
"git-upload-pack",
limits.max_advertisement_bytes.min(limits.max_wire_bytes),
control,
)
.await?;
let mut reader = bytes.as_slice();
let mut wire = Wire {
reader: &mut reader,
remaining: limits.max_wire_bytes,
cancel: control.cancel,
};
let advertisement = protocol::advertise(&mut wire, limits)?;
wire.end()?;
let remaining = limits.max_wire_bytes - advertisement_bytes;
control.check()?;
let mut request = Vec::new();
let negotiation = protocol::request(
&mut request,
&advertisement,
select(&advertisement),
history,
depth,
limits,
control.cancel,
)?;
control.check()?;
if !negotiation.needs_pack {
return Ok(DownloadedFetch {
#[cfg(feature = "tracing")]
trace: crate::trace::DownloadContext::capture(),
advertisement,
negotiation,
known,
limits,
remaining,
body: Vec::new(),
});
}
let mut live = LiveProgress::new(depth.is_some());
let mut validate_prefix = |bytes: &[u8]| {
let mut reader = bytes;
let mut wire = Wire {
reader: &mut reader,
remaining,
cancel: control.cancel,
};
protocol::read_response_prefix(&mut wire, &negotiation, limits).is_ok()
&& wire.end().is_ok()
};
let response = remote
.exchange_observed(
"git-upload-pack",
Some(RequestBody::new(request, Vec::new())),
remaining,
control,
|bytes| live.observe(bytes, &mut validate_prefix, &mut progress),
)
.await;
response.result?;
Ok(DownloadedFetch {
#[cfg(feature = "tracing")]
trace: crate::trace::DownloadContext::capture(),
advertisement,
negotiation,
known,
limits,
remaining,
body: response.body,
})
};
#[cfg(feature = "tracing")]
let result = tracing::Instrument::instrument(operation, span.clone()).await;
#[cfg(not(feature = "tracing"))]
let result = operation.await;
#[cfg(feature = "tracing")]
crate::trace::finish(&span, &result, crate::trace::fetch);
#[cfg(feature = "tracing")]
if let Ok(value) = &result {
span.record("wire_bytes", value.body.len());
}
result
}
struct LiveProgress {
offset: usize,
phase: Phase,
}
enum Phase {
Shallow,
Acknowledgements,
Sideband,
Stopped,
}
impl LiveProgress {
fn new(depth: bool) -> Self {
Self {
offset: 0,
phase: if depth {
Phase::Shallow
} else {
Phase::Acknowledgements
},
}
}
fn observe(
&mut self,
bytes: &[u8],
validate_prefix: &mut impl FnMut(&[u8]) -> bool,
progress: &mut impl FnMut(&[u8]),
) {
while !matches!(self.phase, Phase::Stopped) {
let remaining = &bytes[self.offset..];
let Some(header) = remaining.first_chunk::<4>() else {
return;
};
let Ok(length) = crate::packet::packet_length(header) else {
self.phase = Phase::Stopped;
return;
};
if length == 0 {
self.offset += 4;
self.phase = if matches!(self.phase, Phase::Shallow) {
Phase::Acknowledgements
} else {
Phase::Stopped
};
continue;
}
let Some(packet) = remaining.get(4..length) else {
return;
};
self.offset += length;
match self.phase {
Phase::Shallow => {}
Phase::Acknowledgements => {
let line = packet.strip_suffix(b"\n").unwrap_or(packet);
if line.starts_with(b"ACK ") && line.ends_with(b" continue") {
continue;
}
self.phase = if validate_prefix(&bytes[..self.offset]) {
Phase::Sideband
} else {
Phase::Stopped
};
}
Phase::Sideband => match packet.split_first() {
Some((1, _)) => {}
Some((2, payload)) => progress(payload),
_ => self.phase = Phase::Stopped,
},
Phase::Stopped => unreachable!(),
}
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::LiveProgress;
#[rstest]
#[case::first_header(2)]
#[case::nak(6)]
#[case::sideband_header(10)]
#[case::sideband_payload(13)]
#[case::complete(25)]
fn live_notices_wait_for_complete_prefix_and_packets(#[case] split: usize) {
let bytes = b"0008NAK\n0008\x02a\xff\r0006\x01x0006\x02b0000";
let mut live = LiveProgress::new(false);
let mut messages = Vec::new();
let mut callback = |payload: &[u8]| messages.push(payload.to_vec());
let mut validated = 0;
let mut prefix = |bytes: &[u8]| {
validated += 1;
bytes == b"0008NAK\n"
};
live.observe(&bytes[..split], &mut prefix, &mut callback);
live.observe(bytes, &mut prefix, &mut callback);
live.observe(bytes, &mut prefix, &mut callback);
assert_eq!(validated, 1);
assert_eq!(messages, [b"a\xff\r".to_vec(), b"b".to_vec()]);
}
#[test]
fn live_notices_wait_for_shallow_flush_and_final_ack() {
let prefix = b"000dshallow x00000013ACK x continue\n000aACK x\n";
let bytes = [prefix.as_slice(), b"0006\x02a0000"].concat();
let mut live = LiveProgress::new(true);
let mut messages = Vec::new();
let mut validated = 0;
live.observe(
&bytes[..18],
&mut |_| panic!("incomplete negotiation"),
&mut |_| panic!("early progress"),
);
live.observe(
&bytes,
&mut |bytes| {
validated += 1;
bytes == prefix
},
&mut |bytes| messages.push(bytes.to_vec()),
);
assert_eq!(validated, 1);
assert_eq!(messages, [b"a".to_vec()]);
}
#[rstest]
#[case::invalid_header(b"zzzz")]
#[case::reserved_header(b"0001")]
#[case::flush(b"0000")]
#[case::remote_error(b"0006\x03x")]
#[case::invalid_channel(b"0006\x04x")]
#[case::empty_packet(b"0004")]
fn live_notices_stop_on_terminal_framing(#[case] terminal: &[u8]) {
let bytes = [b"0008NAK\n0006\x02a".as_slice(), terminal, b"0006\x02b"].concat();
let mut messages = Vec::new();
LiveProgress::new(false).observe(&bytes, &mut |_| true, &mut |bytes| {
messages.push(bytes.to_vec())
});
assert_eq!(messages, [b"a".to_vec()]);
}
#[test]
fn live_notices_stop_when_authoritative_prefix_parser_rejects() {
let mut messages = Vec::new();
LiveProgress::new(false).observe(b"0008NAK\n0006\x02a0000", &mut |_| false, &mut |bytes| {
messages.push(bytes.to_vec())
});
assert!(messages.is_empty());
}
}