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