use std::io;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::task::{Context, Poll};
use bytes::Bytes;
use futures::io::AsyncWriteExt as FuturesAsyncWriteExt;
use futures::{AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Stream};
use gix_packetline::async_io::{encode, StreamingPeekableIter};
use gix_packetline::{Channel, PacketLineRef};
use tokio::sync::Semaphore;
use crate::authorize::AuthorizedRepo;
use crate::backend::{GitPackGen, GitPackIngest, GitRefs};
use crate::duplex::{CountingRead, RawTail, Request, STOP_AT_FLUSH, UNKNOWN_COMMAND_DECLINE};
use crate::fetch::{serve_fetch_round, storage_to_session_error, FetchRounds};
use crate::limits::{Limits, SIDEBAND_CHUNK_SIZE};
use crate::push::{serve_receive_pack, PackTailDiscipline};
use crate::substrate::{Service, SubstrateError};
use crate::v0::advertise_v0;
use crate::v2::{advertise_v2, parse_ls_refs_args, serve_ls_refs};
pub struct BodyReader<S, E> {
inner: S,
pending: Bytes,
cursor: usize,
error: PhantomData<fn() -> E>,
}
impl<S, E> BodyReader<S, E>
where
S: Stream<Item = Result<Bytes, E>> + Unpin,
E: std::error::Error + Send + Sync + 'static,
{
pub fn new(inner: S) -> Self {
BodyReader {
inner,
pending: Bytes::new(),
cursor: 0,
error: PhantomData,
}
}
}
impl<S, E> FuturesAsyncRead for BodyReader<S, E>
where
S: Stream<Item = Result<Bytes, E>> + Unpin,
E: std::error::Error + Send + Sync + 'static,
{
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
let this = self.get_mut();
loop {
if this.cursor < this.pending.len() {
let n = buf.len().min(this.pending.len() - this.cursor);
buf[..n].copy_from_slice(&this.pending[this.cursor..this.cursor + n]);
this.cursor += n;
return Poll::Ready(Ok(n));
}
this.pending = Bytes::new();
this.cursor = 0;
match Pin::new(&mut this.inner).poll_next(cx) {
Poll::Ready(Some(Ok(chunk))) => {
if chunk.is_empty() {
continue;
}
this.pending = chunk;
}
Poll::Ready(Some(Err(e))) => return Poll::Ready(Err(io::Error::other(e))),
Poll::Ready(None) => return Poll::Ready(Ok(0)),
Poll::Pending => return Poll::Pending,
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum StatelessPost {
Probe,
Command(Request),
}
pub struct StatelessResponse<W> {
writer: W,
flushed: bool,
}
impl<W> StatelessResponse<W> {
pub(crate) fn new(writer: W) -> Self {
StatelessResponse {
writer,
flushed: false,
}
}
}
impl<W: FuturesAsyncWrite + Unpin> StatelessResponse<W> {
pub fn is_flushed(&self) -> bool {
self.flushed
}
fn after_flush_error(&self) -> SubstrateError {
SubstrateError::Protocol(
"data written after the response flush (the flush is one-shot per response)".to_owned(),
)
}
pub async fn write_text_line(&mut self, line: &[u8]) -> Result<(), SubstrateError> {
if self.flushed {
return Err(self.after_flush_error());
}
encode::text_to_write(line, &mut self.writer).await?;
Ok(())
}
pub async fn write_data_band(&mut self, data: &[u8]) -> Result<(), SubstrateError> {
if self.flushed {
return Err(self.after_flush_error());
}
for chunk in data.chunks(SIDEBAND_CHUNK_SIZE) {
encode::band_to_write(Channel::Data, chunk, &mut self.writer).await?;
}
Ok(())
}
pub async fn write_flush(&mut self) -> Result<(), SubstrateError> {
if self.flushed {
return Ok(());
}
encode::flush_to_write(&mut self.writer).await?;
FuturesAsyncWriteExt::flush(&mut self.writer).await?;
self.flushed = true;
Ok(())
}
}
pub struct StatelessRequest<R, W> {
limits: Limits,
service: Service,
reader: Option<StreamingPeekableIter<CountingRead<R>>>,
response: StatelessResponse<W>,
consumed: Arc<AtomicU64>,
reads: Arc<AtomicU64>,
read_complete: bool,
command_parsed: bool,
terminal: Option<SubstrateError>,
}
impl<R, W> StatelessRequest<R, W>
where
R: FuturesAsyncRead + Unpin,
W: FuturesAsyncWrite + Unpin,
{
pub fn new(reader: R, writer: W, limits: Limits, service: Service) -> Self {
let consumed = Arc::new(AtomicU64::new(0));
let reads = Arc::new(AtomicU64::new(0));
StatelessRequest {
limits,
service,
reader: Some(StreamingPeekableIter::new(
CountingRead {
inner: reader,
consumed: Arc::clone(&consumed),
reads: Arc::clone(&reads),
},
STOP_AT_FLUSH,
false,
)),
response: StatelessResponse {
writer,
flushed: false,
},
consumed,
reads,
read_complete: false,
command_parsed: false,
terminal: None,
}
}
pub fn service(&self) -> Service {
self.service
}
pub fn limits(&self) -> &Limits {
&self.limits
}
pub fn consumed_bytes(&self) -> u64 {
self.consumed.load(Ordering::Relaxed)
}
pub fn transport_reads(&self) -> u64 {
self.reads.load(Ordering::Relaxed)
}
pub fn response(&mut self) -> &mut StatelessResponse<W> {
&mut self.response
}
pub fn take_raw_tail(&mut self) -> Result<RawTail<R>, SubstrateError> {
if let Some(cached) = self.terminal.clone() {
return Err(cached);
}
let Some(reader) = self.reader.take() else {
return Err(SubstrateError::RawTailUnavailable(
"the raw tail was already surrendered — the escape is one-shot",
));
};
if !self.command_parsed {
self.reader = Some(reader);
return Err(SubstrateError::RawTailUnavailable(
"the POST parse has not produced a command — the tail escapes only after the command invocation's terminating flush",
));
}
let counting = reader.into_inner();
Ok(RawTail::new(counting.inner, counting.consumed))
}
pub async fn read_post(&mut self) -> Result<StatelessPost, SubstrateError> {
match self.service {
Service::UploadPack => self.read_post_upload_pack().await,
Service::ReceivePack => self.read_post_receive_pack().await,
}
}
async fn read_post_receive_pack(&mut self) -> Result<StatelessPost, SubstrateError> {
let Self {
limits,
reader,
consumed,
read_complete,
command_parsed,
terminal,
..
} = self;
if let Some(cached) = terminal.clone() {
return Err(cached);
}
if *read_complete {
return Err(SubstrateError::Protocol(
"one command invocation per POST; this body is already parsed".to_owned(),
));
}
let Some(reader) = reader else {
return Err(SubstrateError::RawTailUnavailable(
"the pkt-line reader is gone — the raw tail was surrendered; one command invocation per POST",
));
};
let consumed_entry = consumed.load(Ordering::Relaxed);
enum Event {
Stop,
Text(String),
StopPacket(String),
ErrLine(String),
}
let mut commands: Vec<String> = Vec::new();
let mut args: Vec<String> = Vec::new();
let mut in_options = false;
let mut at_section_start = true;
loop {
let event: Event = match reader.read_line().await {
None => Event::Stop,
Some(Ok(Err(decode_error))) => {
let error = SubstrateError::Protocol(format!(
"malformed pkt-line in the POST body: {decode_error}"
));
*terminal = Some(error.clone());
return Err(error);
}
Some(Err(io_error)) => {
if io_error.kind() == io::ErrorKind::UnexpectedEof && at_section_start {
let untouched = consumed.load(Ordering::Relaxed) == consumed_entry
&& commands.is_empty()
&& args.is_empty();
if untouched || commands.is_empty() {
*read_complete = true;
return Ok(StatelessPost::Probe);
}
if !in_options {
let request = Request {
command: "git-receive-pack".to_owned(),
pre_lines: std::mem::take(&mut commands),
args: Vec::new(),
};
*read_complete = true;
*command_parsed = true;
return Ok(StatelessPost::Command(request));
}
}
let error = SubstrateError::Io(io_error);
*terminal = Some(error.clone());
return Err(error);
}
Some(Ok(Ok(line))) => {
if let Some(message) = line
.check_error()
.map(|e| String::from_utf8_lossy(e.0).into_owned())
{
Event::ErrLine(message)
} else {
match line {
PacketLineRef::Data(d) => {
Event::Text(String::from_utf8_lossy(d).into_owned())
}
PacketLineRef::Delimiter => Event::StopPacket(
"a 0001 delimiter in a receive-pack POST body is not a push shape"
.to_owned(),
),
PacketLineRef::Flush => Event::Stop,
PacketLineRef::ResponseEnd => Event::StopPacket(
"a response-end packet (0002) in a POST body is not a request shape"
.to_owned(),
),
}
}
}
};
match event {
Event::Stop => {
reader.reset();
at_section_start = true;
if commands.is_empty() && !in_options {
*read_complete = true;
return Ok(StatelessPost::Probe);
}
if !in_options
&& commands
.first()
.and_then(|line| line.split_once('\0'))
.map(|(_, caps)| caps.split_whitespace().any(|t| t == "push-options"))
.unwrap_or(false)
{
in_options = true;
continue;
}
let (pre_lines, args) = if in_options {
(std::mem::take(&mut commands), std::mem::take(&mut args))
} else {
(std::mem::take(&mut commands), Vec::new())
};
let request = Request {
command: "git-receive-pack".to_owned(),
pre_lines,
args,
};
*read_complete = true;
*command_parsed = true;
return Ok(StatelessPost::Command(request));
}
Event::StopPacket(message) => {
let error = SubstrateError::Protocol(message);
*terminal = Some(error.clone());
return Err(error);
}
Event::ErrLine(message) => {
let error =
SubstrateError::Protocol(format!("peer sent an ERR line: {message}"));
*terminal = Some(error.clone());
return Err(error);
}
Event::Text(text) => {
let text = text.trim_end_matches(['\n', '\r']).to_owned();
if in_options {
args.push(text);
} else {
commands.push(text);
}
at_section_start = false;
}
}
if consumed.load(Ordering::Relaxed) > limits.max_request_body {
let error = SubstrateError::Protocol(format!(
"the request body exceeds the max_request_body budget ({})",
limits.max_request_body
));
*terminal = Some(error.clone());
return Err(error);
}
}
}
async fn read_post_upload_pack(&mut self) -> Result<StatelessPost, SubstrateError> {
let Self {
limits,
reader,
consumed,
read_complete,
command_parsed,
terminal,
..
} = self;
if let Some(cached) = terminal.clone() {
return Err(cached);
}
if *read_complete {
return Err(SubstrateError::Protocol(
"one command invocation per POST; this body is already parsed".to_owned(),
));
}
let Some(reader) = reader else {
return Err(SubstrateError::RawTailUnavailable(
"the pkt-line reader is gone — the raw tail was surrendered; one command invocation per POST",
));
};
let consumed_entry = consumed.load(Ordering::Relaxed);
enum Event {
Stop,
Text(String),
Delimiter,
StopPacket(String),
ErrLine(String),
}
let mut command: Option<String> = None;
let mut pre_lines: Vec<String> = Vec::new();
let mut args: Vec<String> = Vec::new();
let mut delim_seen = false;
let mut content_seen = false;
let mut at_section_start = true;
loop {
let event: Event = match reader.read_line().await {
None => Event::Stop,
Some(Ok(Err(decode_error))) => {
let error = SubstrateError::Protocol(format!(
"malformed pkt-line in the POST body: {decode_error}"
));
*terminal = Some(error.clone());
return Err(error);
}
Some(Err(io_error)) => {
if io_error.kind() == io::ErrorKind::UnexpectedEof && at_section_start {
let untouched = consumed.load(Ordering::Relaxed) == consumed_entry
&& command.is_none()
&& pre_lines.is_empty()
&& args.is_empty()
&& !delim_seen
&& !content_seen;
if untouched {
*read_complete = true;
return Ok(StatelessPost::Probe);
}
let error = if content_seen {
SubstrateError::Protocol(
"the POST body ended without a command= line".to_owned(),
)
} else {
*read_complete = true;
return Ok(StatelessPost::Probe);
};
*terminal = Some(error.clone());
return Err(error);
}
let error = SubstrateError::Io(io_error);
*terminal = Some(error.clone());
return Err(error);
}
Some(Ok(Ok(line))) => {
if let Some(message) = line
.check_error()
.map(|e| String::from_utf8_lossy(e.0).into_owned())
{
Event::ErrLine(message)
} else {
match line {
PacketLineRef::Data(d) => Event::Text(
String::from_utf8_lossy(d)
.trim_end_matches(['\n', '\r'])
.to_owned(),
),
PacketLineRef::Delimiter => Event::Delimiter,
PacketLineRef::Flush => Event::Stop,
PacketLineRef::ResponseEnd => Event::StopPacket(
"a response-end packet (0002) in a POST body is not a request shape"
.to_owned(),
),
}
}
}
};
match event {
Event::Stop => {
reader.reset();
at_section_start = true;
if let Some(command) = command.take() {
*read_complete = true;
*command_parsed = true;
return Ok(StatelessPost::Command(Request {
command,
pre_lines: std::mem::take(&mut pre_lines),
args: std::mem::take(&mut args),
}));
}
pre_lines.clear();
args.clear();
delim_seen = false;
}
Event::StopPacket(message) => {
let error = SubstrateError::Protocol(message);
*terminal = Some(error.clone());
return Err(error);
}
Event::ErrLine(message) => {
let error =
SubstrateError::Protocol(format!("peer sent an ERR line: {message}"));
*terminal = Some(error.clone());
return Err(error);
}
Event::Text(text) => {
content_seen = true;
at_section_start = false;
if delim_seen {
args.push(text);
} else if let Some(value) = text.strip_prefix("command=") {
if command.is_some() {
let error = SubstrateError::Protocol(
"the POST body carries a second command line".to_owned(),
);
*terminal = Some(error.clone());
return Err(error);
}
if value.is_empty() {
let error = SubstrateError::Protocol("empty command= line".to_owned());
*terminal = Some(error.clone());
return Err(error);
}
command = Some(value.to_owned());
} else {
pre_lines.push(text);
}
}
Event::Delimiter => {
if delim_seen {
let error = SubstrateError::Protocol("repeated 0001 delimiter".to_owned());
*terminal = Some(error.clone());
return Err(error);
}
delim_seen = true;
content_seen = true;
at_section_start = false;
}
}
if consumed.load(Ordering::Relaxed) > limits.max_request_body {
let error = SubstrateError::Protocol(format!(
"the request body exceeds the max_request_body budget ({})",
limits.max_request_body
));
*terminal = Some(error.clone());
return Err(error);
}
}
}
}
#[allow(clippy::too_many_arguments)]
pub async fn serve_stateless_post<R, W>(
repo: &AuthorizedRepo,
reader: R,
writer: W,
limits: Limits,
service: Service,
refs: Arc<dyn GitRefs>,
packgen: Arc<dyn GitPackGen>,
ingest: Arc<dyn GitPackIngest>,
push_options_negotiated: bool,
pipeline: Arc<Semaphore>,
) -> Result<(), SubstrateError>
where
R: FuturesAsyncRead + Unpin + Send + 'static,
W: FuturesAsyncWrite + Unpin + Send,
{
let mut post = StatelessRequest::new(reader, writer, limits, service);
match tokio::time::timeout(
limits.session_wall_clock,
drive_one_post(
repo,
&mut post,
service,
refs,
packgen,
ingest,
push_options_negotiated,
limits,
pipeline,
),
)
.await
{
Ok(result) => result,
Err(_elapsed) => Err(wall_clock_breach(&limits)),
}
}
#[allow(clippy::too_many_arguments)]
async fn drive_one_post<R, W>(
repo: &AuthorizedRepo,
post: &mut StatelessRequest<R, W>,
service: Service,
refs: Arc<dyn GitRefs>,
packgen: Arc<dyn GitPackGen>,
ingest: Arc<dyn GitPackIngest>,
push_options_negotiated: bool,
limits: Limits,
pipeline: Arc<Semaphore>,
) -> Result<(), SubstrateError>
where
R: FuturesAsyncRead + Unpin + Send + 'static,
W: FuturesAsyncWrite + Unpin + Send,
{
match post.read_post().await? {
StatelessPost::Probe => Ok(()),
StatelessPost::Command(request) => match service {
Service::UploadPack => match request.command.as_str() {
"ls-refs" => {
let args = parse_ls_refs_args(&request.args)?;
let listed = refs
.list_refs(repo.record())
.await
.map_err(storage_to_session_error)?;
serve_ls_refs(post.response(), listed, &args, &limits).await
}
"fetch" => {
let mut rounds = FetchRounds::default();
serve_fetch_round(
post.response(),
&request,
&mut rounds,
packgen,
repo.record(),
&limits,
pipeline,
)
.await
}
other => Err(SubstrateError::Protocol(format!(
"unknown command {other:?}{UNKNOWN_COMMAND_DECLINE}"
))),
},
Service::ReceivePack => {
let tail = post.take_raw_tail()?;
serve_receive_pack(
post.response(),
&request,
tail,
push_options_negotiated,
refs,
ingest,
repo.record(),
&limits,
PackTailDiscipline::Stateless,
pipeline,
)
.await
}
},
}
}
fn wall_clock_breach(limits: &Limits) -> SubstrateError {
SubstrateError::Protocol(format!(
"the request exceeded the session_wall_clock budget ({:?}) — server/session budget class, 503-mapped at the door (ADR-009)",
limits.session_wall_clock
))
}
pub async fn serve_stateless_advertisement<W>(
repo: &AuthorizedRepo,
writer: W,
service: Service,
refs: Arc<dyn GitRefs>,
limits: &Limits,
push_options_negotiated: bool,
) -> Result<(), SubstrateError>
where
W: FuturesAsyncWrite + Unpin + Send,
{
match service {
Service::UploadPack => {
let mut writer = writer;
encode::text_to_write(
format!("# service={}", service.as_str()).as_bytes(),
&mut writer,
)
.await?;
encode::flush_to_write(&mut writer).await?;
advertise_v2(&mut StatelessResponse::new(writer)).await
}
Service::ReceivePack => {
let listed = match tokio::time::timeout(
limits.session_wall_clock,
refs.list_refs(repo.record()),
)
.await
{
Ok(Ok(listed)) => listed,
Ok(Err(error)) => return Err(storage_to_session_error(error)),
Err(_elapsed) => return Err(wall_clock_breach(limits)),
};
let mut writer = writer;
encode::text_to_write(
format!("# service={}", service.as_str()).as_bytes(),
&mut writer,
)
.await?;
encode::flush_to_write(&mut writer).await?;
advertise_v0(
&mut StatelessResponse::new(writer),
listed,
push_options_negotiated,
limits,
)
.await
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures::io::AsyncReadExt as FuturesAsyncReadExt;
use futures::stream;
use std::sync::Mutex as StdMutex;
const FLUSH: &[u8] = b"0000";
const DELIMITER: &[u8] = b"0001";
fn pkt(payload: &[u8]) -> Vec<u8> {
let total = payload.len() + 4;
let mut out = Vec::with_capacity(total);
out.extend_from_slice(format!("{total:04x}").as_bytes());
out.extend_from_slice(payload);
out
}
#[derive(Clone, Default)]
struct TestSink(Arc<StdMutex<Vec<u8>>>);
impl futures::AsyncWrite for TestSink {
fn poll_write(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
self.0.lock().unwrap().extend_from_slice(buf);
Poll::Ready(Ok(buf.len()))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
}
type BodyChunks = stream::Iter<std::vec::IntoIter<io::Result<Bytes>>>;
type TestPost = StatelessRequest<BodyReader<BodyChunks, io::Error>, TestSink>;
fn post_with(chunks: Vec<Vec<u8>>, limits: Limits) -> (TestPost, Arc<StdMutex<Vec<u8>>>) {
let sink = TestSink::default();
let items: Vec<io::Result<Bytes>> = chunks
.into_iter()
.map(|c| io::Result::Ok(Bytes::from(c)))
.collect();
let post = StatelessRequest::new(
BodyReader::new(stream::iter(items)),
sink.clone(),
limits,
Service::UploadPack,
);
(post, Arc::clone(&sink.0))
}
fn post(chunks: Vec<Vec<u8>>) -> (TestPost, Arc<StdMutex<Vec<u8>>>) {
post_with(chunks, Limits::default())
}
fn first_post_body(command: &[u8], pre: &[&[u8]], args: &[&[u8]]) -> Vec<u8> {
[
pkt(b"# service=git-upload-pack\n"),
FLUSH.to_vec(),
pkt(b"version 2\n"),
pkt(b"agent=git/2.43\n"),
pkt(b"object-format=sha1\n"),
FLUSH.to_vec(),
pkt(command),
pre.iter().flat_map(|l| pkt(l)).collect::<Vec<u8>>(),
DELIMITER.to_vec(),
args.iter().flat_map(|l| pkt(l)).collect::<Vec<u8>>(),
FLUSH.to_vec(),
]
.concat()
}
fn command_post_body(command: &[u8], pre: &[&[u8]], args: &[&[u8]]) -> Vec<u8> {
[
pkt(command),
pre.iter().flat_map(|l| pkt(l)).collect::<Vec<u8>>(),
DELIMITER.to_vec(),
args.iter().flat_map(|l| pkt(l)).collect::<Vec<u8>>(),
FLUSH.to_vec(),
]
.concat()
}
fn parse_pkts(buffer: &[u8]) -> Vec<Vec<u8>> {
let mut out = Vec::new();
let mut i = 0;
while i < buffer.len() {
assert!(i + 4 <= buffer.len(), "trailing partial header");
let len =
usize::from_str_radix(std::str::from_utf8(&buffer[i..i + 4]).unwrap(), 16).unwrap();
if len == 0 {
out.push(Vec::new());
i += 4;
} else {
assert!(len >= 4 && i + len <= buffer.len(), "partial pkt-line");
out.push(buffer[i + 4..i + len].to_vec());
i += len;
}
}
out
}
#[tokio::test]
async fn stateless_constructs_from_reader_writer_limits_service() {
let (mut post, _) = post(vec![command_post_body(b"command=ls-refs\n", &[], &[])]);
assert_eq!(post.service(), Service::UploadPack);
assert_eq!(*post.limits(), Limits::default());
assert_eq!(post.consumed_bytes(), 0);
assert_eq!(post.transport_reads(), 0);
assert!(!post.response().is_flushed());
}
#[tokio::test]
async fn capability_dump_is_skipped_and_the_arg_split_is_delim_aware() {
let (mut post, _) = post(vec![first_post_body(
b"command=ls-refs\n",
&[b"object-format=sha1"],
&[b"peel", b"ref-prefix refs/heads/"],
)]);
let parsed = post.read_post().await.unwrap();
let StatelessPost::Command(request) = parsed else {
panic!("the first POST parses as a command: {parsed:?}")
};
assert_eq!(request.command, "ls-refs");
assert_eq!(request.pre_lines, vec!["object-format=sha1"]);
assert_eq!(request.args, vec!["peel", "ref-prefix refs/heads/"]);
assert!(post.consumed_bytes() > 0);
}
#[tokio::test]
async fn subsequent_posts_parse_the_command_directly() {
let (mut post, _) = post(vec![command_post_body(
b"command=fetch\n",
&[b"agent=git/2.43"],
&[b"have deadbeef", b"done"],
)]);
let parsed = post.read_post().await.unwrap();
let StatelessPost::Command(request) = parsed else {
panic!("a negotiation-round POST parses as a command: {parsed:?}")
};
assert_eq!(request.command, "fetch");
assert_eq!(request.pre_lines, vec!["agent=git/2.43"]);
assert_eq!(request.args, vec!["have deadbeef", "done"]);
}
#[tokio::test]
async fn the_body_across_dribbled_chunks_parses_streaming() {
let body = first_post_body(b"command=ls-refs\n", &[], &[b"peel"]);
let (mut post, _) = post(vec![
body[..40].to_vec(),
body[40..90].to_vec(),
body[90..].to_vec(),
]);
let parsed = post.read_post().await.unwrap();
let StatelessPost::Command(request) = parsed else {
panic!("chunked bodies parse like a whole one: {parsed:?}")
};
assert_eq!(request.command, "ls-refs");
assert_eq!(request.args, vec!["peel"]);
}
#[tokio::test]
async fn flush_only_post_resolves_to_the_probe_shape() {
let (mut post, _) = post(vec![FLUSH.to_vec()]);
let parsed = post.read_post().await.unwrap();
assert_eq!(parsed, StatelessPost::Probe);
let error = post.read_post().await.unwrap_err();
assert!(error
.to_string()
.contains("one command invocation per POST"));
}
#[tokio::test]
async fn empty_post_body_resolves_to_the_probe_shape() {
let (mut post, _) = post(vec![Vec::new()]);
let parsed = post.read_post().await.unwrap();
assert_eq!(parsed, StatelessPost::Probe);
}
#[tokio::test]
async fn the_probe_answer_takes_the_response_rules() {
let (mut post, buffer) = post(vec![FLUSH.to_vec()]);
post.read_post().await.unwrap();
post.response().write_flush().await.unwrap();
post.response().write_flush().await.unwrap();
assert_eq!(&*buffer.lock().unwrap(), FLUSH, "a bare flush, once");
assert!(post.response().is_flushed());
}
#[tokio::test]
async fn body_ending_without_a_command_line_is_a_protocol_error() {
let (mut post, _) = post(vec![[
pkt(b"# service=git-upload-pack\n"),
FLUSH.to_vec(),
pkt(b"version 2\n"),
FLUSH.to_vec(),
]
.concat()]);
let error = post.read_post().await.unwrap_err();
assert!(
error.is_terminal(),
"a truncated dump is terminal: {error:?}"
);
match error {
SubstrateError::Protocol(message) => {
assert!(message.contains("command"), "{message}");
}
other => panic!("a command-less body is out of grammar: {other:?}"),
}
assert!(post.read_post().await.is_err(), "terminal stays terminal");
}
#[tokio::test]
async fn truncated_bodies_break_unconditionally() {
let full = command_post_body(b"command=ls-refs\n", &[], &[b"peel"]);
let (mut post, _) = post(vec![full[..full.len() - 6].to_vec()]);
let error = post.read_post().await.unwrap_err();
let SubstrateError::Io(e) = &error else {
panic!("a mid-section truncation is terminal io: {error:?}");
};
assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof);
assert!(error.is_terminal());
let reads_frozen = post.transport_reads();
for _ in 0..5 {
assert!(post.read_post().await.is_err(), "terminal stays terminal");
}
assert_eq!(
post.transport_reads(),
reads_frozen,
"no retry may re-enter the transport"
);
}
#[tokio::test]
async fn malformed_pkt_lines_are_protocol_errors() {
let (mut post, _) = post(vec![b"zzzz".to_vec()]);
let error = post.read_post().await.unwrap_err();
assert!(
error.is_terminal(),
"decode failures are terminal: {error:?}"
);
match error {
SubstrateError::Protocol(message) => {
assert!(message.contains("malformed pkt-line"), "{message}");
}
other => panic!("decode errors are protocol-shaped: {other:?}"),
}
}
#[tokio::test]
async fn an_err_line_in_the_body_is_a_protocol_error() {
let (mut post, _) = post(vec![pkt(b"ERR malformed client")]);
let error = post.read_post().await.unwrap_err();
match error {
SubstrateError::Protocol(message) => {
assert!(message.contains("ERR"), "{message}");
assert!(message.contains("malformed client"), "{message}");
}
other => panic!("client ERR lines are protocol-shaped: {other:?}"),
}
}
#[tokio::test]
async fn a_response_end_packet_in_the_body_is_a_protocol_error() {
let (mut post, _) = post(vec![b"0002".to_vec()]);
let error = post.read_post().await.unwrap_err();
match error {
SubstrateError::Protocol(message) => {
assert!(message.contains("response-end"), "{message}");
}
other => panic!("0002 in a POST body is out of grammar: {other:?}"),
}
}
#[tokio::test]
async fn repeated_delimiter_is_a_protocol_error() {
let (mut post, _) = post(vec![[
pkt(b"command=ls-refs\n"),
DELIMITER.to_vec(),
DELIMITER.to_vec(),
pkt(b"peel\n"),
FLUSH.to_vec(),
]
.concat()]);
let error = post.read_post().await.unwrap_err();
match error {
SubstrateError::Protocol(message) => {
assert!(message.contains("repeated"), "{message}");
}
other => panic!("a repeated delimiter is out of grammar: {other:?}"),
}
}
#[tokio::test]
async fn a_second_command_line_is_a_protocol_error() {
let (mut post, _) = post(vec![command_post_body(
b"command=ls-refs\n",
&[b"command=fetch"],
&[],
)]);
let error = post.read_post().await.unwrap_err();
match error {
SubstrateError::Protocol(message) => {
assert!(message.contains("second command"), "{message}");
}
other => panic!("a second command line is out of grammar: {other:?}"),
}
}
#[tokio::test]
async fn the_request_body_budget_fails_closed() {
let body = first_post_body(b"command=ls-refs\n", &[], &[b"peel"]);
let (mut post, _) = post_with(
vec![body],
Limits {
max_request_body: 10,
..Limits::default()
},
);
let error = post.read_post().await.unwrap_err();
assert!(
error.is_terminal(),
"a budget breach fails closed: {error:?}"
);
match error {
SubstrateError::Protocol(message) => {
assert!(message.contains("max_request_body"), "{message}");
}
other => panic!("a budget breach is protocol-shaped: {other:?}"),
}
}
#[tokio::test]
async fn body_reader_serves_chunks_as_reads() {
let stream = stream::iter([
Ok::<Bytes, io::Error>(Bytes::from_static(b"hello ")),
Ok(Bytes::from_static(b"world")),
Err(io::Error::other("body failed")),
]);
let mut reader = BodyReader::new(stream);
let mut got = vec![0u8; 3];
reader.read_exact(&mut got).await.unwrap();
assert_eq!(got, b"hel".to_vec());
let n = reader.read(&mut got).await.unwrap();
assert_eq!(&got[..n], b"lo ", "short reads stop at the cursor");
let mut rest = Vec::new();
reader
.read_to_end(&mut rest)
.await
.expect_err("the failing chunk surfaces as an io error");
}
#[tokio::test]
async fn body_reader_reads_to_clean_eof() {
let stream = stream::iter([Ok::<Bytes, io::Error>(Bytes::from_static(b"abcd"))]);
let mut reader = BodyReader::new(stream);
let mut got = Vec::new();
reader.read_to_end(&mut got).await.unwrap();
assert_eq!(got, b"abcd".to_vec());
}
#[tokio::test]
async fn responses_end_at_flush_and_never_0002() {
let (mut post, buffer) = post(vec![command_post_body(b"command=fetch\n", &[], &[b"done"])]);
post.read_post().await.unwrap();
post.response().write_text_line(b"packfile").await.unwrap();
post.response()
.write_data_band(&vec![b'x'; SIDEBAND_CHUNK_SIZE + 10])
.await
.unwrap();
post.response().write_flush().await.unwrap();
let body = buffer.lock().unwrap().clone();
let pkts = parse_pkts(&body);
assert_eq!(pkts[0], b"packfile\n".to_vec());
assert_eq!(pkts[1][0], 1, "band 1");
assert_eq!(pkts[1].len() - 1, SIDEBAND_CHUNK_SIZE);
assert_eq!(pkts[2][0], 1, "band 1");
assert_eq!(pkts[2].len() - 1, 10);
assert!(
pkts.last().unwrap().is_empty(),
"the response ends at flush"
);
assert!(
!body.windows(4).any(|w| w == b"0002"),
"no 0002 anywhere in a response body"
);
}
#[tokio::test]
async fn the_response_flush_is_one_shot_and_data_after_it_is_refused() {
let (mut post, buffer) = post(vec![command_post_body(b"command=ls-refs\n", &[], &[])]);
post.read_post().await.unwrap();
post.response().write_text_line(b"version 2").await.unwrap();
post.response().write_flush().await.unwrap();
assert!(post.response().is_flushed());
let snapshot = buffer.lock().unwrap().clone();
post.response().write_flush().await.unwrap();
assert_eq!(
&*buffer.lock().unwrap(),
&snapshot,
"the second flush is a no-op"
);
let error = post.response().write_text_line(b"after").await.unwrap_err();
match error {
SubstrateError::Protocol(message) => {
assert!(message.contains("one-shot"), "{message}");
}
other => panic!("data after the flush is refused: {other:?}"),
}
let error = post.response().write_data_band(b"after").await.unwrap_err();
match error {
SubstrateError::Protocol(_) => {}
other => panic!("band data after the flush is refused: {other:?}"),
}
}
#[tokio::test]
async fn data_band_writes_chunk_at_the_cap() {
let (mut post, buffer) = post(vec![command_post_body(b"command=fetch\n", &[], &[b"done"])]);
post.read_post().await.unwrap();
let payload = vec![b'x'; SIDEBAND_CHUNK_SIZE * 2 + 20000];
post.response().write_data_band(&payload).await.unwrap();
post.response().write_flush().await.unwrap();
let body = buffer.lock().unwrap().clone();
let pkts = parse_pkts(&body);
let sizes: Vec<usize> = pkts.iter().map(|p| p.len()).collect();
assert_eq!(
sizes,
vec![1 + 65000, 1 + 65000, 1 + 20000, 0],
"band-1 chunk sizes then the flush"
);
assert!(
pkts.iter().all(|p| p.is_empty() || p[0] == 1),
"band 1 only"
);
}
#[tokio::test]
async fn the_machinery_runs_over_arbitrary_reader_writer_ends() {
use tokio::io::AsyncWriteExt as TokioAsyncWriteExt;
let (mut peer, stream) = tokio::io::duplex(1 << 15);
TokioAsyncWriteExt::write_all(
&mut peer,
&first_post_body(b"command=ls-refs\n", &[], &[b"peel"]),
)
.await
.unwrap();
let sink = TestSink::default();
let mut post = StatelessRequest::new(
tokio_util::compat::TokioAsyncReadCompatExt::compat(stream),
sink.clone(),
Limits::default(),
Service::UploadPack,
);
let parsed = post.read_post().await.unwrap();
let StatelessPost::Command(request) = parsed else {
panic!("tokio-compat ends parse identically")
};
assert_eq!(request.command, "ls-refs");
assert_eq!(request.args, vec!["peel"]);
post.response().write_flush().await.unwrap();
assert_eq!(&*sink.0.lock().unwrap(), FLUSH);
drop(peer);
}
#[tokio::test]
async fn raw_tail_escapes_after_the_command_flush_without_byte_loss() {
let tail: Vec<u8> = (0..6000u32).map(|i| (i % 251) as u8).collect();
let command = command_post_body(b"command=ls-refs\n", &[], &[b"peel"]);
let body = [&command[..], &tail].concat();
let command_len = command.len();
let (mut post, _) = post_with(
vec![
body[..12].to_vec(),
body[12..command_len - 2].to_vec(),
body[command_len - 2..command_len + 2].to_vec(),
body[command_len + 2..command_len + 40].to_vec(),
body[command_len + 40..].to_vec(),
],
Limits::default(),
);
let parsed = post.read_post().await.unwrap();
let StatelessPost::Command(request) = parsed else {
panic!("the command invocation parses: {parsed:?}")
};
assert_eq!(request.command, "ls-refs");
assert_eq!(
post.consumed_bytes(),
command_len as u64,
"the parse consumed exactly the framed body"
);
let mut tail_reader = post.take_raw_tail().unwrap();
let mut got = Vec::new();
tail_reader.read_to_end(&mut got).await.unwrap();
assert_eq!(got, tail, "the tail continues byte-exactly from the flush");
assert_eq!(
tail_reader.consumed_bytes(),
body.len() as u64,
"the tail rides the body counter: framed + tail"
);
assert_eq!(
tail_reader.consumed_bytes(),
post.consumed_bytes(),
"one shared counter, reconciled"
);
}
#[tokio::test]
async fn the_stateless_tail_escape_refuses_without_a_command_parse() {
let (mut post, _) = post(vec![FLUSH.to_vec()]);
let error = post.take_raw_tail().unwrap_err();
match error {
SubstrateError::RawTailUnavailable(message) => {
assert!(message.contains("command"), "{message}");
}
other => panic!("the escape before a parse is misuse: {other:?}"),
}
let parsed = post.read_post().await.unwrap();
assert_eq!(parsed, StatelessPost::Probe);
let error = post.take_raw_tail().unwrap_err();
assert!(
matches!(error, SubstrateError::RawTailUnavailable(_)),
"a probe body surrenders no tail: {error:?}"
);
assert!(post.read_post().await.is_err());
}
#[tokio::test]
async fn the_stateless_tail_escape_is_one_shot_and_the_reader_is_gone() {
let (mut post, _) = post(vec![command_post_body(b"command=ls-refs\n", &[], &[])]);
let StatelessPost::Command(_) = post.read_post().await.unwrap() else {
panic!("the command invocation parses")
};
let _tail = post.take_raw_tail().unwrap();
let error = post.take_raw_tail().unwrap_err();
match error {
SubstrateError::RawTailUnavailable(message) => {
assert!(message.contains("one-shot"), "{message}");
}
other => panic!("a second escape is misuse: {other:?}"),
}
let error = post.read_post().await.unwrap_err();
match error {
SubstrateError::Protocol(message) => {
assert!(
message.contains("one command invocation per POST"),
"{message}"
);
}
other => panic!("post-escape reads are refused: {other:?}"),
}
}
#[tokio::test]
async fn the_stateless_tail_escape_refuses_after_a_terminal_error() {
let full = command_post_body(b"command=ls-refs\n", &[], &[b"peel"]);
let (mut post, _) = post(vec![full[..full.len() - 6].to_vec()]);
let terminal = post.read_post().await.unwrap_err();
assert!(terminal.is_terminal());
let error = post.take_raw_tail().unwrap_err();
match (terminal, error) {
(SubstrateError::Io(first), SubstrateError::Io(second)) => {
assert_eq!(first.kind(), io::ErrorKind::UnexpectedEof);
assert_eq!(second.kind(), io::ErrorKind::UnexpectedEof);
}
other => panic!("the terminal error stays terminal: {other:?}"),
}
}
}