1use core::fmt;
4
5use crate::Method;
6
7pub const MAX_REQUEST_TARGET_BYTES: usize = 8192;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum RequestTargetError {
13 Empty,
15 NotOriginForm,
17 TooLong,
19 InvalidByte,
22}
23
24#[derive(Clone, Copy, Eq, PartialEq)]
26pub struct RequestTarget<'a> {
27 value: &'a str,
28}
29
30impl<'a> RequestTarget<'a> {
31 pub fn new(value: &'a str) -> Result<Self, RequestTargetError> {
33 if value.is_empty() {
34 return Err(RequestTargetError::Empty);
35 }
36 if value.len() > MAX_REQUEST_TARGET_BYTES {
37 return Err(RequestTargetError::TooLong);
38 }
39 if !value.starts_with('/') || value.starts_with("//") {
40 return Err(RequestTargetError::NotOriginForm);
41 }
42 if !value
43 .bytes()
44 .all(|byte| byte.is_ascii_graphic() && byte != b'#' && byte != b'\\')
45 {
46 return Err(RequestTargetError::InvalidByte);
47 }
48 Ok(Self { value })
49 }
50
51 #[must_use]
53 pub const fn as_str(self) -> &'a str {
54 self.value
55 }
56}
57
58impl fmt::Debug for RequestTarget<'_> {
59 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60 formatter.write_str("RequestTarget([redacted])")
61 }
62}
63
64#[derive(Clone, Copy)]
66pub struct TransportRequest<'a> {
67 method: Method,
68 target: RequestTarget<'a>,
69 body: &'a [u8],
70}
71
72impl<'a> TransportRequest<'a> {
73 #[must_use]
75 pub const fn new(method: Method, target: RequestTarget<'a>) -> Self {
76 Self {
77 method,
78 target,
79 body: &[],
80 }
81 }
82
83 #[must_use]
85 pub const fn with_body(mut self, body: &'a [u8]) -> Self {
86 self.body = body;
87 self
88 }
89
90 #[must_use]
92 pub const fn method(self) -> Method {
93 self.method
94 }
95
96 #[must_use]
98 pub const fn target(self) -> RequestTarget<'a> {
99 self.target
100 }
101
102 #[must_use]
104 pub const fn body(self) -> &'a [u8] {
105 self.body
106 }
107}
108
109impl fmt::Debug for TransportRequest<'_> {
110 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
111 formatter
112 .debug_struct("TransportRequest")
113 .field("method", &self.method)
114 .field("target", &self.target)
115 .field("body", &"[redacted]")
116 .finish()
117 }
118}
119
120#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
122pub struct StatusCode(u16);
123
124impl StatusCode {
125 pub const OK: Self = Self(200);
127 pub const TOO_MANY_REQUESTS: Self = Self(429);
129
130 #[must_use]
132 pub const fn new(value: u16) -> Option<Self> {
133 if value < 100 || value > 599 {
134 return None;
135 }
136 Some(Self(value))
137 }
138
139 #[must_use]
141 pub const fn get(self) -> u16 {
142 self.0
143 }
144
145 #[must_use]
147 pub const fn is_success(self) -> bool {
148 self.0 >= 200 && self.0 <= 299
149 }
150
151 #[must_use]
153 pub const fn is_error(self) -> bool {
154 self.0 >= 400
155 }
156}
157
158#[derive(Clone, Copy)]
170pub struct TransportResponse<'buffer> {
171 status: StatusCode,
172 body: &'buffer [u8],
173}
174
175impl<'buffer> TransportResponse<'buffer> {
176 #[must_use]
178 pub const fn new(status: StatusCode, body: &'buffer [u8]) -> Self {
179 Self { status, body }
180 }
181
182 #[must_use]
184 pub const fn status(&self) -> StatusCode {
185 self.status
186 }
187
188 #[must_use]
190 pub const fn body(&self) -> &'buffer [u8] {
191 self.body
192 }
193}
194
195impl fmt::Debug for TransportResponse<'_> {
196 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
197 formatter
198 .debug_struct("TransportResponse")
199 .field("status", &self.status)
200 .field("body_len", &self.body.len())
201 .field("body", &"[redacted]")
202 .finish()
203 }
204}
205
206pub trait BlockingTransport {
211 type Error;
213
214 fn send<'buffer>(
216 &mut self,
217 request: TransportRequest<'_>,
218 response_body: &'buffer mut [u8],
219 ) -> Result<TransportResponse<'buffer>, Self::Error>;
220}
221
222#[cfg(test)]
223mod tests {
224 use super::{
225 RequestTarget, RequestTargetError, StatusCode, TransportRequest, TransportResponse,
226 };
227 use crate::Method;
228 use core::fmt::Write;
229
230 #[test]
231 fn request_targets_are_origin_form_and_bounded() {
232 let target = RequestTarget::new("/servers?page=2");
233 assert_eq!(target.map(RequestTarget::as_str), Ok("/servers?page=2"));
234 assert_eq!(
235 RequestTarget::new("https://example.invalid/servers"),
236 Err(RequestTargetError::NotOriginForm)
237 );
238 assert_eq!(
239 RequestTarget::new("//evil.example/steal"),
240 Err(RequestTargetError::NotOriginForm)
241 );
242 assert_eq!(
243 RequestTarget::new("///evil.example/steal"),
244 Err(RequestTargetError::NotOriginForm)
245 );
246 assert_eq!(
247 RequestTarget::new("/servers#fragment"),
248 Err(RequestTargetError::InvalidByte)
249 );
250 assert_eq!(
251 RequestTarget::new("/\\evil"),
252 Err(RequestTargetError::InvalidByte)
253 );
254 assert_eq!(RequestTarget::new(""), Err(RequestTargetError::Empty));
255 assert_eq!(
256 RequestTarget::new("/servers bad"),
257 Err(RequestTargetError::InvalidByte)
258 );
259 let mut accepted = [b'a'; super::MAX_REQUEST_TARGET_BYTES];
260 if let Some(first) = accepted.first_mut() {
261 *first = b'/';
262 }
263 let accepted = core::str::from_utf8(&accepted);
264 assert!(accepted.is_ok());
265 if let Ok(accepted) = accepted {
266 assert!(RequestTarget::new(accepted).is_ok());
267 }
268 let mut rejected = [b'a'; super::MAX_REQUEST_TARGET_BYTES + 1];
269 if let Some(first) = rejected.first_mut() {
270 *first = b'/';
271 }
272 let rejected = core::str::from_utf8(&rejected);
273 assert!(rejected.is_ok());
274 if let Ok(rejected) = rejected {
275 assert_eq!(
276 RequestTarget::new(rejected),
277 Err(RequestTargetError::TooLong)
278 );
279 }
280 }
281
282 #[test]
283 fn transport_request_debug_redacts_target_and_body() {
284 let target = RequestTarget::new("/servers?token=secret");
285 if let Ok(target) = target {
286 let request = TransportRequest::new(Method::Post, target).with_body(b"secret-body");
287 let mut debug = DebugBuffer::new();
288 assert!(write!(&mut debug, "{request:?}").is_ok());
289 let debug = debug.as_str();
290 assert!(debug.contains("[redacted]"));
291 assert!(!debug.contains("secret"));
292 }
293 }
294
295 #[test]
296 fn status_codes_are_bounded_and_classified() {
297 assert_eq!(StatusCode::new(99), None);
298 assert!(StatusCode::new(204).is_some_and(StatusCode::is_success));
299 assert!(StatusCode::new(429).is_some_and(StatusCode::is_error));
300 assert_eq!(StatusCode::new(600), None);
301 }
302
303 #[test]
304 fn transport_response_borrows_exact_initialized_body_and_redacts_debug() {
305 let output = b"secret-response-trailing-capacity";
306 let body = output.get(..15).unwrap_or_default();
307 let response = TransportResponse::new(StatusCode::OK, body);
308
309 assert_eq!(response.status(), StatusCode::OK);
310 assert_eq!(response.body(), b"secret-response");
311
312 let mut debug = DebugBuffer::new();
313 assert!(write!(&mut debug, "{response:?}").is_ok());
314 let debug = debug.as_str();
315 assert!(debug.contains("body_len: 15"));
316 assert!(debug.contains("[redacted]"));
317 assert!(!debug.contains("secret-response"));
318 }
319
320 struct DebugBuffer {
321 bytes: [u8; 192],
322 len: usize,
323 }
324
325 impl DebugBuffer {
326 const fn new() -> Self {
327 Self {
328 bytes: [0; 192],
329 len: 0,
330 }
331 }
332
333 fn as_str(&self) -> &str {
334 core::str::from_utf8(self.bytes.get(..self.len).unwrap_or_default()).unwrap_or_default()
335 }
336 }
337
338 impl Write for DebugBuffer {
339 fn write_str(&mut self, value: &str) -> core::fmt::Result {
340 let end = self.len.checked_add(value.len()).ok_or(core::fmt::Error)?;
341 let target = self.bytes.get_mut(self.len..end).ok_or(core::fmt::Error)?;
342 target.copy_from_slice(value.as_bytes());
343 self.len = end;
344 Ok(())
345 }
346 }
347}