use core::fmt;
use crate::Method;
pub const MAX_REQUEST_TARGET_BYTES: usize = 8192;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RequestTargetError {
Empty,
NotOriginForm,
TooLong,
InvalidByte,
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct RequestTarget<'a> {
value: &'a str,
}
impl<'a> RequestTarget<'a> {
pub fn new(value: &'a str) -> Result<Self, RequestTargetError> {
if value.is_empty() {
return Err(RequestTargetError::Empty);
}
if value.len() > MAX_REQUEST_TARGET_BYTES {
return Err(RequestTargetError::TooLong);
}
if !value.starts_with('/') || value.starts_with("//") {
return Err(RequestTargetError::NotOriginForm);
}
if !value
.bytes()
.all(|byte| byte.is_ascii_graphic() && byte != b'#' && byte != b'\\')
{
return Err(RequestTargetError::InvalidByte);
}
Ok(Self { value })
}
#[must_use]
pub const fn as_str(self) -> &'a str {
self.value
}
}
impl fmt::Debug for RequestTarget<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("RequestTarget([redacted])")
}
}
#[derive(Clone, Copy)]
pub struct TransportRequest<'a> {
method: Method,
target: RequestTarget<'a>,
body: &'a [u8],
}
impl<'a> TransportRequest<'a> {
#[must_use]
pub const fn new(method: Method, target: RequestTarget<'a>) -> Self {
Self {
method,
target,
body: &[],
}
}
#[must_use]
pub const fn with_body(mut self, body: &'a [u8]) -> Self {
self.body = body;
self
}
#[must_use]
pub const fn method(self) -> Method {
self.method
}
#[must_use]
pub const fn target(self) -> RequestTarget<'a> {
self.target
}
#[must_use]
pub const fn body(self) -> &'a [u8] {
self.body
}
}
impl fmt::Debug for TransportRequest<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransportRequest")
.field("method", &self.method)
.field("target", &self.target)
.field("body", &"[redacted]")
.finish()
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct StatusCode(u16);
impl StatusCode {
pub const OK: Self = Self(200);
pub const TOO_MANY_REQUESTS: Self = Self(429);
#[must_use]
pub const fn new(value: u16) -> Option<Self> {
if value < 100 || value > 599 {
return None;
}
Some(Self(value))
}
#[must_use]
pub const fn get(self) -> u16 {
self.0
}
#[must_use]
pub const fn is_success(self) -> bool {
self.0 >= 200 && self.0 <= 299
}
#[must_use]
pub const fn is_error(self) -> bool {
self.0 >= 400
}
}
#[derive(Clone, Copy)]
pub struct TransportResponse<'buffer> {
status: StatusCode,
body: &'buffer [u8],
}
impl<'buffer> TransportResponse<'buffer> {
#[must_use]
pub const fn new(status: StatusCode, body: &'buffer [u8]) -> Self {
Self { status, body }
}
#[must_use]
pub const fn status(&self) -> StatusCode {
self.status
}
#[must_use]
pub const fn body(&self) -> &'buffer [u8] {
self.body
}
}
impl fmt::Debug for TransportResponse<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransportResponse")
.field("status", &self.status)
.field("body_len", &self.body.len())
.field("body", &"[redacted]")
.finish()
}
}
pub trait BlockingTransport {
type Error;
fn send<'buffer>(
&mut self,
request: TransportRequest<'_>,
response_body: &'buffer mut [u8],
) -> Result<TransportResponse<'buffer>, Self::Error>;
}
#[cfg(test)]
mod tests {
use super::{
RequestTarget, RequestTargetError, StatusCode, TransportRequest, TransportResponse,
};
use crate::Method;
use core::fmt::Write;
#[test]
fn request_targets_are_origin_form_and_bounded() {
let target = RequestTarget::new("/servers?page=2");
assert_eq!(target.map(RequestTarget::as_str), Ok("/servers?page=2"));
assert_eq!(
RequestTarget::new("https://example.invalid/servers"),
Err(RequestTargetError::NotOriginForm)
);
assert_eq!(
RequestTarget::new("//evil.example/steal"),
Err(RequestTargetError::NotOriginForm)
);
assert_eq!(
RequestTarget::new("///evil.example/steal"),
Err(RequestTargetError::NotOriginForm)
);
assert_eq!(
RequestTarget::new("/servers#fragment"),
Err(RequestTargetError::InvalidByte)
);
assert_eq!(
RequestTarget::new("/\\evil"),
Err(RequestTargetError::InvalidByte)
);
assert_eq!(RequestTarget::new(""), Err(RequestTargetError::Empty));
assert_eq!(
RequestTarget::new("/servers bad"),
Err(RequestTargetError::InvalidByte)
);
let mut accepted = [b'a'; super::MAX_REQUEST_TARGET_BYTES];
if let Some(first) = accepted.first_mut() {
*first = b'/';
}
let accepted = core::str::from_utf8(&accepted);
assert!(accepted.is_ok());
if let Ok(accepted) = accepted {
assert!(RequestTarget::new(accepted).is_ok());
}
let mut rejected = [b'a'; super::MAX_REQUEST_TARGET_BYTES + 1];
if let Some(first) = rejected.first_mut() {
*first = b'/';
}
let rejected = core::str::from_utf8(&rejected);
assert!(rejected.is_ok());
if let Ok(rejected) = rejected {
assert_eq!(
RequestTarget::new(rejected),
Err(RequestTargetError::TooLong)
);
}
}
#[test]
fn transport_request_debug_redacts_target_and_body() {
let target = RequestTarget::new("/servers?token=secret");
if let Ok(target) = target {
let request = TransportRequest::new(Method::Post, target).with_body(b"secret-body");
let mut debug = DebugBuffer::new();
assert!(write!(&mut debug, "{request:?}").is_ok());
let debug = debug.as_str();
assert!(debug.contains("[redacted]"));
assert!(!debug.contains("secret"));
}
}
#[test]
fn status_codes_are_bounded_and_classified() {
assert_eq!(StatusCode::new(99), None);
assert!(StatusCode::new(204).is_some_and(StatusCode::is_success));
assert!(StatusCode::new(429).is_some_and(StatusCode::is_error));
assert_eq!(StatusCode::new(600), None);
}
#[test]
fn transport_response_borrows_exact_initialized_body_and_redacts_debug() {
let output = b"secret-response-trailing-capacity";
let body = output.get(..15).unwrap_or_default();
let response = TransportResponse::new(StatusCode::OK, body);
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.body(), b"secret-response");
let mut debug = DebugBuffer::new();
assert!(write!(&mut debug, "{response:?}").is_ok());
let debug = debug.as_str();
assert!(debug.contains("body_len: 15"));
assert!(debug.contains("[redacted]"));
assert!(!debug.contains("secret-response"));
}
struct DebugBuffer {
bytes: [u8; 192],
len: usize,
}
impl DebugBuffer {
const fn new() -> Self {
Self {
bytes: [0; 192],
len: 0,
}
}
fn as_str(&self) -> &str {
core::str::from_utf8(self.bytes.get(..self.len).unwrap_or_default()).unwrap_or_default()
}
}
impl Write for DebugBuffer {
fn write_str(&mut self, value: &str) -> core::fmt::Result {
let end = self.len.checked_add(value.len()).ok_or(core::fmt::Error)?;
let target = self.bytes.get_mut(self.len..end).ok_or(core::fmt::Error)?;
target.copy_from_slice(value.as_bytes());
self.len = end;
Ok(())
}
}
}