1use core::fmt;
4
5use crate::Method;
6
7pub const MAX_REQUEST_TARGET_BYTES: usize = 8192;
9
10pub const MAX_CONTENT_TYPE_BYTES: usize = 128;
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub enum ContentTypeError {
16 Empty,
18 TooLong,
20 Invalid,
23}
24
25#[derive(Clone, Copy, Eq, PartialEq)]
27pub struct ContentType<'a> {
28 value: &'a str,
29}
30
31impl fmt::Debug for ContentType<'_> {
32 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33 formatter.write_str("ContentType([redacted])")
34 }
35}
36
37impl<'a> ContentType<'a> {
38 pub const JSON: Self = Self {
40 value: "application/json",
41 };
42
43 pub fn new(value: &'a str) -> Result<Self, ContentTypeError> {
45 if value.is_empty() {
46 return Err(ContentTypeError::Empty);
47 }
48 if value.len() > MAX_CONTENT_TYPE_BYTES {
49 return Err(ContentTypeError::TooLong);
50 }
51 if !value.bytes().all(|byte| (b' '..=b'~').contains(&byte)) {
52 return Err(ContentTypeError::Invalid);
53 }
54 let essence = value.split(';').next().unwrap_or_default();
55 let Some((media_type, subtype)) = essence.split_once('/') else {
56 return Err(ContentTypeError::Invalid);
57 };
58 if media_type.is_empty()
59 || subtype.is_empty()
60 || !media_type.bytes().all(is_http_token_byte)
61 || !subtype.bytes().all(is_http_token_byte)
62 {
63 return Err(ContentTypeError::Invalid);
64 }
65 Ok(Self { value })
66 }
67
68 #[must_use]
70 pub const fn as_str(self) -> &'a str {
71 self.value
72 }
73}
74
75const fn is_http_token_byte(byte: u8) -> bool {
76 byte.is_ascii_alphanumeric()
77 || matches!(
78 byte,
79 b'!' | b'#'
80 | b'$'
81 | b'%'
82 | b'&'
83 | b'\''
84 | b'*'
85 | b'+'
86 | b'-'
87 | b'.'
88 | b'^'
89 | b'_'
90 | b'`'
91 | b'|'
92 | b'~'
93 )
94}
95
96#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub enum RequestTargetError {
99 Empty,
101 NotOriginForm,
103 TooLong,
105 InvalidByte,
108}
109
110#[derive(Clone, Copy, Eq, PartialEq)]
112pub struct RequestTarget<'a> {
113 value: &'a str,
114}
115
116impl<'a> RequestTarget<'a> {
117 pub fn new(value: &'a str) -> Result<Self, RequestTargetError> {
119 if value.is_empty() {
120 return Err(RequestTargetError::Empty);
121 }
122 if value.len() > MAX_REQUEST_TARGET_BYTES {
123 return Err(RequestTargetError::TooLong);
124 }
125 if !value.starts_with('/') || value.starts_with("//") {
126 return Err(RequestTargetError::NotOriginForm);
127 }
128 if !value
129 .bytes()
130 .all(|byte| byte.is_ascii_graphic() && byte != b'#' && byte != b'\\')
131 {
132 return Err(RequestTargetError::InvalidByte);
133 }
134 Ok(Self { value })
135 }
136
137 #[must_use]
139 pub const fn as_str(self) -> &'a str {
140 self.value
141 }
142}
143
144impl fmt::Debug for RequestTarget<'_> {
145 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
146 formatter.write_str("RequestTarget([redacted])")
147 }
148}
149
150#[derive(Clone, Copy)]
152pub struct TransportRequest<'a> {
153 method: Method,
154 target: RequestTarget<'a>,
155 body: &'a [u8],
156 content_type: Option<ContentType<'a>>,
157}
158
159impl<'a> TransportRequest<'a> {
160 #[must_use]
162 pub const fn new(method: Method, target: RequestTarget<'a>) -> Self {
163 Self {
164 method,
165 target,
166 body: &[],
167 content_type: None,
168 }
169 }
170
171 #[must_use]
173 pub const fn with_body(mut self, body: &'a [u8]) -> Self {
174 self.body = body;
175 self
176 }
177
178 #[must_use]
180 pub const fn with_content_type(mut self, content_type: ContentType<'a>) -> Self {
181 self.content_type = Some(content_type);
182 self
183 }
184
185 #[must_use]
187 pub const fn method(self) -> Method {
188 self.method
189 }
190
191 #[must_use]
193 pub const fn target(self) -> RequestTarget<'a> {
194 self.target
195 }
196
197 #[must_use]
199 pub const fn body(self) -> &'a [u8] {
200 self.body
201 }
202
203 #[must_use]
205 pub const fn content_type(self) -> Option<ContentType<'a>> {
206 self.content_type
207 }
208}
209
210impl fmt::Debug for TransportRequest<'_> {
211 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
212 formatter
213 .debug_struct("TransportRequest")
214 .field("method", &self.method)
215 .field("target", &self.target)
216 .field("body", &"[redacted]")
217 .field("content_type", &self.content_type)
218 .finish()
219 }
220}
221
222#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
224pub struct StatusCode(u16);
225
226impl StatusCode {
227 pub const OK: Self = Self(200);
229 pub const TOO_MANY_REQUESTS: Self = Self(429);
231
232 #[must_use]
234 pub const fn new(value: u16) -> Option<Self> {
235 if value < 100 || value > 599 {
236 return None;
237 }
238 Some(Self(value))
239 }
240
241 #[must_use]
243 pub const fn get(self) -> u16 {
244 self.0
245 }
246
247 #[must_use]
249 pub const fn is_success(self) -> bool {
250 self.0 >= 200 && self.0 <= 299
251 }
252
253 #[must_use]
255 pub const fn is_error(self) -> bool {
256 self.0 >= 400
257 }
258}
259
260#[derive(Clone, Copy)]
272pub struct TransportResponse<'buffer> {
273 status: StatusCode,
274 body: &'buffer [u8],
275}
276
277impl<'buffer> TransportResponse<'buffer> {
278 #[must_use]
280 pub const fn new(status: StatusCode, body: &'buffer [u8]) -> Self {
281 Self { status, body }
282 }
283
284 #[must_use]
286 pub const fn status(&self) -> StatusCode {
287 self.status
288 }
289
290 #[must_use]
292 pub const fn body(&self) -> &'buffer [u8] {
293 self.body
294 }
295}
296
297impl fmt::Debug for TransportResponse<'_> {
298 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
299 formatter
300 .debug_struct("TransportResponse")
301 .field("status", &self.status)
302 .field("body_len", &self.body.len())
303 .field("body", &"[redacted]")
304 .finish()
305 }
306}
307
308pub trait BlockingTransport {
313 type Error;
315
316 fn send<'buffer>(
318 &mut self,
319 request: TransportRequest<'_>,
320 response_body: &'buffer mut [u8],
321 ) -> Result<TransportResponse<'buffer>, Self::Error>;
322}
323
324#[cfg(test)]
325mod tests {
326 use super::{
327 ContentType, ContentTypeError, RequestTarget, RequestTargetError, StatusCode,
328 TransportRequest, TransportResponse,
329 };
330 use crate::Method;
331 use core::fmt::Write;
332
333 #[test]
334 fn request_targets_are_origin_form_and_bounded() {
335 let target = RequestTarget::new("/servers?page=2");
336 assert_eq!(target.map(RequestTarget::as_str), Ok("/servers?page=2"));
337 assert_eq!(
338 RequestTarget::new("https://example.invalid/servers"),
339 Err(RequestTargetError::NotOriginForm)
340 );
341 assert_eq!(
342 RequestTarget::new("//evil.example/steal"),
343 Err(RequestTargetError::NotOriginForm)
344 );
345 assert_eq!(
346 RequestTarget::new("///evil.example/steal"),
347 Err(RequestTargetError::NotOriginForm)
348 );
349 assert_eq!(
350 RequestTarget::new("/servers#fragment"),
351 Err(RequestTargetError::InvalidByte)
352 );
353 assert_eq!(
354 RequestTarget::new("/\\evil"),
355 Err(RequestTargetError::InvalidByte)
356 );
357 assert_eq!(RequestTarget::new(""), Err(RequestTargetError::Empty));
358 assert_eq!(
359 RequestTarget::new("/servers bad"),
360 Err(RequestTargetError::InvalidByte)
361 );
362 let mut accepted = [b'a'; super::MAX_REQUEST_TARGET_BYTES];
363 if let Some(first) = accepted.first_mut() {
364 *first = b'/';
365 }
366 let accepted = core::str::from_utf8(&accepted);
367 assert!(accepted.is_ok());
368 if let Ok(accepted) = accepted {
369 assert!(RequestTarget::new(accepted).is_ok());
370 }
371 let mut rejected = [b'a'; super::MAX_REQUEST_TARGET_BYTES + 1];
372 if let Some(first) = rejected.first_mut() {
373 *first = b'/';
374 }
375 let rejected = core::str::from_utf8(&rejected);
376 assert!(rejected.is_ok());
377 if let Ok(rejected) = rejected {
378 assert_eq!(
379 RequestTarget::new(rejected),
380 Err(RequestTargetError::TooLong)
381 );
382 }
383 }
384
385 #[test]
386 fn transport_request_debug_redacts_target_and_body() {
387 let target = RequestTarget::new("/servers?token=secret");
388 if let Ok(target) = target {
389 let content_type = ContentType::new("application/x-private; token=secret-content");
390 assert!(content_type.is_ok());
391 let request = TransportRequest::new(Method::Post, target).with_body(b"secret-body");
392 let request = content_type.map_or(request, |value| request.with_content_type(value));
393 let mut debug = DebugBuffer::new();
394 assert!(write!(&mut debug, "{request:?}").is_ok());
395 let debug = debug.as_str();
396 assert!(debug.contains("[redacted]"));
397 assert!(!debug.contains("secret"));
398 assert!(!debug.contains("application/x-private"));
399 }
400 }
401
402 #[test]
403 fn content_types_are_bounded_and_header_safe() {
404 assert_eq!(
405 ContentType::new("application/json").map(ContentType::as_str),
406 Ok("application/json")
407 );
408 assert_eq!(
409 ContentType::new("text/plain; charset=utf-8").map(ContentType::as_str),
410 Ok("text/plain; charset=utf-8")
411 );
412 assert_eq!(ContentType::new(""), Err(ContentTypeError::Empty));
413 assert_eq!(
414 ContentType::new("application"),
415 Err(ContentTypeError::Invalid)
416 );
417 assert_eq!(
418 ContentType::new("application/json\r\nx-evil: true"),
419 Err(ContentTypeError::Invalid)
420 );
421 let oversized = [b'a'; super::MAX_CONTENT_TYPE_BYTES + 1];
422 let oversized = core::str::from_utf8(&oversized);
423 assert!(oversized.is_ok());
424 if let Ok(oversized) = oversized {
425 assert_eq!(ContentType::new(oversized), Err(ContentTypeError::TooLong));
426 }
427 }
428
429 #[test]
430 fn transport_requests_preserve_explicit_content_type() {
431 let target = RequestTarget::new("/servers");
432 if let Ok(target) = target {
433 let request = TransportRequest::new(Method::Post, target)
434 .with_body(b"{}")
435 .with_content_type(ContentType::JSON);
436 assert_eq!(request.content_type(), Some(ContentType::JSON));
437 }
438 }
439
440 #[test]
441 fn status_codes_are_bounded_and_classified() {
442 assert_eq!(StatusCode::new(99), None);
443 assert!(StatusCode::new(204).is_some_and(StatusCode::is_success));
444 assert!(StatusCode::new(429).is_some_and(StatusCode::is_error));
445 assert_eq!(StatusCode::new(600), None);
446 }
447
448 #[test]
449 fn transport_response_borrows_exact_initialized_body_and_redacts_debug() {
450 let output = b"secret-response-trailing-capacity";
451 let body = output.get(..15).unwrap_or_default();
452 let response = TransportResponse::new(StatusCode::OK, body);
453
454 assert_eq!(response.status(), StatusCode::OK);
455 assert_eq!(response.body(), b"secret-response");
456
457 let mut debug = DebugBuffer::new();
458 assert!(write!(&mut debug, "{response:?}").is_ok());
459 let debug = debug.as_str();
460 assert!(debug.contains("body_len: 15"));
461 assert!(debug.contains("[redacted]"));
462 assert!(!debug.contains("secret-response"));
463 }
464
465 struct DebugBuffer {
466 bytes: [u8; 192],
467 len: usize,
468 }
469
470 impl DebugBuffer {
471 const fn new() -> Self {
472 Self {
473 bytes: [0; 192],
474 len: 0,
475 }
476 }
477
478 fn as_str(&self) -> &str {
479 core::str::from_utf8(self.bytes.get(..self.len).unwrap_or_default()).unwrap_or_default()
480 }
481 }
482
483 impl Write for DebugBuffer {
484 fn write_str(&mut self, value: &str) -> core::fmt::Result {
485 let end = self.len.checked_add(value.len()).ok_or(core::fmt::Error)?;
486 let target = self.bytes.get_mut(self.len..end).ok_or(core::fmt::Error)?;
487 target.copy_from_slice(value.as_bytes());
488 self.len = end;
489 Ok(())
490 }
491 }
492}