use anyhow::Result;
use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt};
use crate::pool::PooledBuffer;
use crate::protocol::RequestContext;
pub(crate) use crate::session::multiline_framing::BackendResponseOrder;
pub(crate) struct BackendReadResult {
inner: BackendReadResultInner,
}
#[derive(Debug)]
pub(crate) enum SingleLineReplyReadError {
Full { bytes_read: usize },
Io(std::io::Error),
Closed,
Invalid { bytes_read: usize },
}
enum BackendReadResultInner {
Response(crate::session::multiline_framing::BackendResponseRead),
Invalid(crate::session::multiline_framing::ResponseReadError),
}
pub(crate) async fn read_single_line_reply<C>(
conn: &mut C,
request: &RequestContext,
scratch: &mut [u8],
) -> Result<String, SingleLineReplyReadError>
where
C: tokio::io::AsyncRead + Unpin,
{
let mut total = 0usize;
loop {
if total == scratch.len() {
return Err(SingleLineReplyReadError::Full { bytes_read: total });
}
let n = conn
.read(&mut scratch[total..])
.await
.map_err(SingleLineReplyReadError::Io)?;
if n == 0 {
return Err(SingleLineReplyReadError::Closed);
}
total += n;
match crate::session::multiline_framing::unpacked_single_line_response(
request,
&scratch[..total],
) {
Ok(bytes) => {
return Ok(String::from_utf8_lossy(bytes).into_owned());
}
Err(crate::session::multiline_framing::ResponseReadError::Incomplete) => {}
Err(crate::session::multiline_framing::ResponseReadError::Invalid(_)) => {
return Err(SingleLineReplyReadError::Invalid { bytes_read: total });
}
}
}
}
#[must_use]
pub(crate) fn cached_response_completion() -> std::io::IoSlice<'static> {
crate::session::multiline_framing::cached_response_completion()
}
#[must_use]
pub(crate) fn captured_multiline_payload_body(payload: &[u8]) -> Option<&[u8]> {
crate::session::multiline_framing::captured_multiline_payload_body(payload)
}
#[must_use]
pub(crate) const fn capabilities_response(auth_enabled: bool) -> &'static [u8] {
if auth_enabled {
crate::session::multiline_framing::CAPABILITIES_WITH_AUTHINFO_RESPONSE
} else {
crate::session::multiline_framing::CAPABILITIES_WITHOUT_AUTHINFO_RESPONSE
}
}
#[must_use]
pub(crate) const fn capabilities_without_authinfo_response() -> &'static [u8] {
crate::session::multiline_framing::CAPABILITIES_WITHOUT_AUTHINFO_RESPONSE
}
impl BackendReadResult {
#[must_use]
pub(crate) fn status_code(&self) -> Option<crate::protocol::StatusCode> {
match &self.inner {
BackendReadResultInner::Response(response) => Some(response.status_code()),
BackendReadResultInner::Invalid(_) => None,
}
}
#[must_use]
pub(crate) fn single_line_bytes<'a>(&self, buffer: &'a PooledBuffer) -> Option<&'a [u8]> {
match &self.inner {
BackendReadResultInner::Response(response) => response.single_line_bytes(buffer),
BackendReadResultInner::Invalid(_) => None,
}
}
pub(crate) fn log_warnings(
&self,
buffer: &[u8],
client_addr: impl std::fmt::Display,
backend_id: crate::types::BackendId,
) {
if let BackendReadResultInner::Invalid(err) = &self.inner {
err.log_warnings(buffer, client_addr, backend_id);
}
}
}
fn duration_micros_u64(duration: std::time::Duration) -> u64 {
u64::try_from(duration.as_micros()).unwrap_or(u64::MAX)
}
async fn read_until_backend_reply<C>(
conn: &mut C,
request: &RequestContext,
buffer: &mut PooledBuffer,
) -> Result<BackendReadResult>
where
C: AsyncReadExt + Unpin,
{
loop {
match crate::session::multiline_framing::backend_response_read(request, buffer) {
Ok(response) => {
return Ok(BackendReadResult {
inner: BackendReadResultInner::Response(response),
});
}
Err(err @ crate::session::multiline_framing::ResponseReadError::Invalid(_)) => {
return Ok(BackendReadResult {
inner: BackendReadResultInner::Invalid(err),
});
}
Err(crate::session::multiline_framing::ResponseReadError::Incomplete) => {
let more = buffer.read_more(conn).await?;
if more == 0 {
anyhow::bail!(
"Backend EOF before complete backend response ({} bytes)",
buffer.initialized()
);
}
}
}
}
}
#[must_use]
pub fn format_hex_preview(data: &[u8], max_bytes: usize) -> String {
let preview = &data[..data.len().min(max_bytes)];
preview
.iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<_>>()
.join(" ")
}
pub(crate) async fn send_request<C>(
conn: &mut C,
request: &RequestContext,
buffer: &mut PooledBuffer,
) -> Result<BackendReadResult>
where
C: AsyncReadExt + AsyncWriteExt + Unpin,
{
request.write_wire_to(conn).await?;
let n = buffer.read_from(conn).await?;
if n == 0 {
anyhow::bail!("Backend connection closed unexpectedly");
}
read_until_backend_reply(conn, request, buffer).await
}
pub(crate) async fn send_request_timed<C>(
conn: &mut C,
request: &RequestContext,
buffer: &mut PooledBuffer,
) -> Result<(BackendReadResult, u64, u64, u64)>
where
C: AsyncReadExt + AsyncWriteExt + Unpin,
{
use std::time::Instant;
let start = Instant::now();
request.write_wire_to(conn).await?;
let after_send = Instant::now();
let n = buffer.read_from(conn).await?;
if n == 0 {
anyhow::bail!("Backend connection closed unexpectedly");
}
let response = read_until_backend_reply(conn, request, buffer).await?;
let after_recv = Instant::now();
let send_elapsed = after_send.duration_since(start);
let recv_elapsed = after_recv.duration_since(after_send);
let elapsed = after_recv.duration_since(start);
Ok((
response,
duration_micros_u64(elapsed),
duration_micros_u64(send_elapsed),
duration_micros_u64(recv_elapsed),
))
}
pub(crate) async fn write_response_with_optional_capture<W>(
request: &RequestContext,
buffer: &mut PooledBuffer,
conn: &mut crate::stream::ConnectionStream,
writer: &mut W,
captured: &mut crate::pool::ChunkedResponse,
pool: &crate::pool::BufferPool,
backend_id: crate::types::BackendId,
) -> Result<(u64, bool), crate::session::response_transfer::ResponseTransferError>
where
W: AsyncWrite + Unpin,
{
crate::session::multiline_framing::write_response_with_optional_capture(
request, buffer, conn, writer, captured, pool, backend_id,
)
.await
}
pub(crate) async fn observe_response(
request: &RequestContext,
buffer: &mut PooledBuffer,
conn: &mut crate::stream::ConnectionStream,
pool: &crate::pool::BufferPool,
backend_id: crate::types::BackendId,
) -> Result<(), crate::session::response_transfer::ResponseTransferError> {
crate::session::multiline_framing::observe_response(request, buffer, conn, pool, backend_id)
.await
}
pub(crate) async fn capture_complete_multiline_response(
conn: &mut crate::stream::ConnectionStream,
buffer: &mut PooledBuffer,
capture: &mut PooledBuffer,
) -> anyhow::Result<()> {
crate::session::multiline_framing::capture_isolated_multiline_response(conn, buffer, capture)
.await
}
pub(crate) async fn capture_complete_multiline_response_chunked_optional(
conn: &mut crate::stream::ConnectionStream,
buffer: &mut PooledBuffer,
pool: &crate::pool::BufferPool,
response: &mut crate::pool::ChunkedResponse,
) -> anyhow::Result<bool> {
crate::session::multiline_framing::capture_isolated_multiline_response_chunked_optional(
conn, buffer, pool, response,
)
.await
.map_err(|err| anyhow::anyhow!("backend multiline response capture failed: {err:?}"))
}
pub(crate) async fn observe_complete_multiline_response(
conn: &mut crate::stream::ConnectionStream,
buffer: &mut PooledBuffer,
) -> anyhow::Result<()> {
crate::session::multiline_framing::observe_isolated_multiline_response(conn, buffer)
.await
.map_err(|err| anyhow::anyhow!("backend multiline response drain failed: {err:?}"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::StatusCode;
use std::collections::VecDeque;
use std::pin::Pin;
use std::task::{Context, Poll};
struct ChunkedStream {
chunks: VecDeque<Vec<u8>>,
written: Vec<u8>,
}
impl ChunkedStream {
fn new(chunks: Vec<Vec<u8>>) -> Self {
Self {
chunks: chunks.into(),
written: Vec::new(),
}
}
}
impl tokio::io::AsyncRead for ChunkedStream {
fn poll_read(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
if let Some(chunk) = self.chunks.pop_front() {
let len = chunk.len().min(buf.remaining());
buf.put_slice(&chunk[..len]);
if len < chunk.len() {
self.chunks.push_front(chunk[len..].to_vec());
}
}
Poll::Ready(Ok(()))
}
}
impl tokio::io::AsyncWrite for ChunkedStream {
fn poll_write(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
self.written.extend_from_slice(buf);
Poll::Ready(Ok(buf.len()))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
}
#[tokio::test]
async fn test_send_request_partial_read_accumulates() {
let mut stream = ChunkedStream::new(vec![b"20".to_vec(), b"0 OK\r\n".to_vec()]);
let pool = crate::pool::BufferPool::for_tests();
let mut buffer = pool.acquire();
let request = RequestContext::from_verb_args(b"DATE", b"");
let response = send_request(&mut stream, &request, &mut buffer)
.await
.expect("send_request should handle partial reads");
let status_code = response
.status_code()
.expect("DATE response should be valid");
assert_eq!(status_code, StatusCode::new(200));
assert!(!request.has_response_body(status_code));
}
#[tokio::test]
async fn test_send_request_reads_complete_initial_reply_when_code_arrives_first() {
let mut stream = ChunkedStream::new(vec![b"111".to_vec(), b" 20260501173336\r\n".to_vec()]);
let pool = crate::pool::BufferPool::for_tests();
let mut buffer = pool.acquire();
let request = RequestContext::from_verb_args(b"DATE", b"");
let response = send_request(&mut stream, &request, &mut buffer)
.await
.expect("send_request should read through complete backend response");
let status_code = response
.status_code()
.expect("DATE response should be valid");
assert_eq!(status_code, StatusCode::new(111));
assert!(!request.has_response_body(status_code));
assert_eq!(&buffer[..buffer.initialized()], b"111 20260501173336\r\n");
}
#[tokio::test]
async fn test_send_request_single_byte_reads() {
let data = b"211 Group\r\n";
let chunks: Vec<Vec<u8>> = data.iter().map(|&b| vec![b]).collect();
let mut stream = ChunkedStream::new(chunks);
let pool = crate::pool::BufferPool::for_tests();
let mut buffer = pool.acquire();
let request = RequestContext::from_verb_args(b"GROUP", b"alt.test");
let response = send_request(&mut stream, &request, &mut buffer)
.await
.expect("send_request should handle single-byte reads");
let status_code = response
.status_code()
.expect("GROUP response should be valid");
assert_eq!(status_code, StatusCode::new(211));
assert!(!request.has_response_body(status_code));
}
#[tokio::test]
async fn read_single_line_reply_uses_caller_scratch_for_split_reply() {
let mut stream = ChunkedStream::new(vec![b"111 20260501".to_vec(), b"173336\r\n".to_vec()]);
let request = RequestContext::from_verb_args(b"DATE", b"");
let mut scratch = [0u8; 32];
let response = read_single_line_reply(&mut stream, &request, &mut scratch)
.await
.expect("split single-line reply should complete");
assert_eq!(response, "111 20260501173336\r\n");
assert_eq!(&scratch[..response.len()], response.as_bytes());
}
#[tokio::test]
async fn read_single_line_reply_reports_full_scratch_with_bytes_read() {
let mut stream = ChunkedStream::new(vec![b"111 ".to_vec()]);
let request = RequestContext::from_verb_args(b"DATE", b"");
let mut scratch = [0u8; 4];
let err = read_single_line_reply(&mut stream, &request, &mut scratch)
.await
.expect_err("unterminated reply should fill scratch");
assert!(matches!(
err,
SingleLineReplyReadError::Full { bytes_read: 4 }
));
assert_eq!(&scratch, b"111 ");
}
#[tokio::test]
async fn read_single_line_reply_reports_invalid_bytes_read() {
let mut stream = ChunkedStream::new(vec![b"abc\r\n".to_vec()]);
let request = RequestContext::from_verb_args(b"DATE", b"");
let mut scratch = [0u8; 16];
let err = read_single_line_reply(&mut stream, &request, &mut scratch)
.await
.expect_err("nonnumeric reply should be invalid");
assert!(matches!(
err,
SingleLineReplyReadError::Invalid { bytes_read: 5 }
));
assert_eq!(&scratch[..5], b"abc\r\n");
}
#[tokio::test]
async fn read_single_line_reply_rejects_packed_trailing_bytes() {
let packed_reply = b"111 20260501173336\r\n222 next\r\n";
let mut stream = ChunkedStream::new(vec![packed_reply.to_vec()]);
let request = RequestContext::from_verb_args(b"DATE", b"");
let mut scratch = [0u8; 64];
let err = read_single_line_reply(&mut stream, &request, &mut scratch)
.await
.expect_err("packed setup reply must not wait for more bytes");
assert!(matches!(
err,
SingleLineReplyReadError::Invalid { bytes_read } if bytes_read == packed_reply.len()
));
}
#[test]
fn test_format_hex_preview_empty() {
let data = b"";
let result = format_hex_preview(data, 256);
assert_eq!(result, "");
}
#[test]
fn test_format_hex_preview_small() {
let data = b"ABC";
let result = format_hex_preview(data, 256);
assert_eq!(result, "41 42 43");
}
#[test]
fn test_format_hex_preview_respects_max_bytes() {
let data = b"ABCDEFGH";
let result = format_hex_preview(data, 4);
assert_eq!(result, "41 42 43 44");
}
#[test]
fn test_format_hex_preview_response_bytes() {
let data = b"430 No such article\r\n";
let result = format_hex_preview(data, 256);
assert!(result.starts_with("34 33 30 20")); assert!(result.ends_with("0d 0a")); assert_eq!(result.split_whitespace().count(), data.len());
}
#[test]
fn test_format_hex_preview_non_ascii() {
let data = &[0xFF, 0xFE, 0x00, 0x01];
let result = format_hex_preview(data, 256);
assert_eq!(result, "ff fe 00 01");
}
#[test]
fn test_format_hex_preview_256_bytes() {
let data: Vec<u8> = (0..=255).chain(0..44).collect();
assert_eq!(data.len(), 300);
let result = format_hex_preview(&data, 256);
assert_eq!(result.split_whitespace().count(), 256);
assert!(result.ends_with("ff"));
}
}