1use core::fmt;
4
5use crate::rate_limit::RateLimit;
6use crate::transport::{
7 MediaType, ResponseBuffer, ResponseContentType, ResponseWriterError, StatusCode,
8 TransportResponse,
9};
10
11#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13pub enum ResponseBodyPolicy {
14 Required,
16 Optional,
18 Forbidden,
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum ContentTypePolicy {
25 Required(&'static [MediaType<'static>]),
27 Optional(&'static [MediaType<'static>]),
29 Forbidden,
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub enum ResponsePolicyValidationError {
36 MissingSuccessStatus,
38 NonSuccessStatus,
40 DuplicateSuccessStatus,
42 MissingAcceptedMediaType,
44 DuplicateAcceptedMediaType,
46 RequiredBodyHasZeroLimit,
48 ForbiddenBodyHasNonzeroLimit,
50 ForbiddenBodyAllowsContentType,
52 RequiredBodyForbidsContentType,
54}
55
56impl_static_error!(ResponsePolicyValidationError,
57 Self::MissingSuccessStatus => "response policy has no success status",
58 Self::NonSuccessStatus => "response policy contains a non-success status",
59 Self::DuplicateSuccessStatus => "response policy contains duplicate statuses",
60 Self::MissingAcceptedMediaType => "response policy has no accepted media type",
61 Self::DuplicateAcceptedMediaType => "response policy contains duplicate media types",
62 Self::RequiredBodyHasZeroLimit => "required response body has a zero limit",
63 Self::ForbiddenBodyHasNonzeroLimit => "forbidden response body has a nonzero limit",
64 Self::ForbiddenBodyAllowsContentType => "forbidden response body allows a content type",
65 Self::RequiredBodyForbidsContentType => "required response body forbids its content type",
66);
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub enum ResponsePolicyError {
71 UnexpectedStatus,
73 BodyTooLarge,
75 MissingBody,
77 ForbiddenBody,
79 MissingContentType,
81 UnexpectedContentType,
83 ForbiddenContentType,
85 UncommittedResponse,
87}
88
89impl_static_error!(ResponsePolicyError,
90 Self::UnexpectedStatus => "response status is not expected",
91 Self::BodyTooLarge => "response body exceeds the operation limit",
92 Self::MissingBody => "required response body is missing",
93 Self::ForbiddenBody => "response body is forbidden",
94 Self::MissingContentType => "required response content type is missing",
95 Self::UnexpectedContentType => "response content type is not accepted",
96 Self::ForbiddenContentType => "response content type is forbidden",
97 Self::UncommittedResponse => "response writer is not committed",
98);
99
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
102pub struct ResponsePolicy {
103 success_statuses: &'static [StatusCode],
104 content_type: ContentTypePolicy,
105 body: ResponseBodyPolicy,
106 max_body_bytes: usize,
107}
108
109impl ResponsePolicy {
110 pub fn new(
112 success_statuses: &'static [StatusCode],
113 content_type: ContentTypePolicy,
114 body: ResponseBodyPolicy,
115 max_body_bytes: usize,
116 ) -> Result<Self, ResponsePolicyValidationError> {
117 validate_statuses(success_statuses)?;
118 validate_media_types(content_type)?;
119 match (body, content_type, max_body_bytes) {
120 (ResponseBodyPolicy::Required, _, 0) => {
121 return Err(ResponsePolicyValidationError::RequiredBodyHasZeroLimit);
122 }
123 (ResponseBodyPolicy::Forbidden, _, limit) if limit != 0 => {
124 return Err(ResponsePolicyValidationError::ForbiddenBodyHasNonzeroLimit);
125 }
126 (
127 ResponseBodyPolicy::Forbidden,
128 ContentTypePolicy::Required(_) | ContentTypePolicy::Optional(_),
129 _,
130 ) => {
131 return Err(ResponsePolicyValidationError::ForbiddenBodyAllowsContentType);
132 }
133 (ResponseBodyPolicy::Required, ContentTypePolicy::Forbidden, _) => {
134 return Err(ResponsePolicyValidationError::RequiredBodyForbidsContentType);
135 }
136 _ => {}
137 }
138 Ok(Self {
139 success_statuses,
140 content_type,
141 body,
142 max_body_bytes,
143 })
144 }
145
146 #[must_use]
148 pub const fn success_statuses(self) -> &'static [StatusCode] {
149 self.success_statuses
150 }
151
152 #[must_use]
154 pub const fn content_type_policy(self) -> ContentTypePolicy {
155 self.content_type
156 }
157
158 #[must_use]
160 pub const fn body_policy(self) -> ResponseBodyPolicy {
161 self.body
162 }
163
164 #[must_use]
166 pub const fn max_body_bytes(self) -> usize {
167 self.max_body_bytes
168 }
169
170 pub fn validate<'buffer, 'sanitizer, S>(
172 self,
173 writer: ResponseBuffer<'buffer, 'sanitizer, S>,
174 ) -> Result<CheckedResponseGuard<'buffer, 'sanitizer, S>, ResponsePolicyError>
175 where
176 S: crate::transport::ResponseStorageSanitizer + ?Sized,
177 {
178 let snapshot = {
179 let response = writer.response().map_err(map_writer_error)?;
180 self.validate_view(response)?
181 };
182 Ok(CheckedResponseGuard { writer, snapshot })
183 }
184
185 fn validate_view(
186 self,
187 response: TransportResponse<'_>,
188 ) -> Result<CheckedResponseSnapshot, ResponsePolicyError> {
189 if !self.success_statuses.contains(&response.status()) {
190 return Err(ResponsePolicyError::UnexpectedStatus);
191 }
192 match self.body {
193 ResponseBodyPolicy::Forbidden if !response.body().is_empty() => {
194 return Err(ResponsePolicyError::ForbiddenBody);
195 }
196 _ => {}
197 }
198 if response.body().len() > self.max_body_bytes {
199 return Err(ResponsePolicyError::BodyTooLarge);
200 }
201 if matches!(self.body, ResponseBodyPolicy::Required) && response.body().is_empty() {
202 return Err(ResponsePolicyError::MissingBody);
203 }
204 validate_content_type(self.content_type, response.content_type())?;
205 Ok(CheckedResponseSnapshot {
206 status: response.status(),
207 body_len: response.body().len(),
208 content_type: response.content_type(),
209 rate_limit: response.rate_limit(),
210 })
211 }
212}
213
214#[derive(Clone, Copy)]
216pub struct CheckedResponse<'body> {
217 status: StatusCode,
218 body: &'body [u8],
219 content_type: Option<ResponseContentType>,
220 rate_limit: Option<RateLimit>,
221}
222
223impl CheckedResponse<'_> {
224 #[must_use]
226 pub const fn status(&self) -> StatusCode {
227 self.status
228 }
229
230 #[must_use]
232 pub const fn body(&self) -> &[u8] {
233 self.body
234 }
235
236 #[must_use]
238 pub const fn content_type(&self) -> Option<ResponseContentType> {
239 self.content_type
240 }
241
242 #[must_use]
244 pub const fn rate_limit(&self) -> Option<RateLimit> {
245 self.rate_limit
246 }
247}
248
249#[derive(Clone, Copy)]
250struct CheckedResponseSnapshot {
251 status: StatusCode,
252 body_len: usize,
253 content_type: Option<ResponseContentType>,
254 rate_limit: Option<RateLimit>,
255}
256
257pub struct CheckedResponseGuard<
262 'buffer,
263 'sanitizer,
264 S: crate::transport::ResponseStorageSanitizer + ?Sized,
265> {
266 writer: ResponseBuffer<'buffer, 'sanitizer, S>,
267 snapshot: CheckedResponseSnapshot,
268}
269
270impl<S> CheckedResponseGuard<'_, '_, S>
271where
272 S: crate::transport::ResponseStorageSanitizer + ?Sized,
273{
274 #[must_use]
276 pub const fn status(&self) -> StatusCode {
277 self.snapshot.status
278 }
279
280 #[must_use]
282 pub const fn content_type(&self) -> Option<ResponseContentType> {
283 self.snapshot.content_type
284 }
285
286 #[must_use]
288 pub const fn rate_limit(&self) -> Option<RateLimit> {
289 self.snapshot.rate_limit
290 }
291
292 pub fn with_borrowed<R>(
308 &self,
309 inspect: impl for<'response> FnOnce(CheckedResponse<'response>) -> R,
310 ) -> R {
311 inspect(self.checked_response())
312 }
313
314 pub fn decode_owned<R, E>(
316 self,
317 decode: impl for<'response> FnOnce(CheckedResponse<'response>) -> Result<R, E>,
318 ) -> Result<R, E> {
319 let result = decode(self.checked_response());
320 drop(self);
321 result
322 }
323
324 fn checked_response(&self) -> CheckedResponse<'_> {
325 CheckedResponse {
326 status: self.snapshot.status,
327 body: self.writer.initialized_body(self.snapshot.body_len),
328 content_type: self.snapshot.content_type,
329 rate_limit: self.snapshot.rate_limit,
330 }
331 }
332}
333
334impl<S> fmt::Debug for CheckedResponseGuard<'_, '_, S>
335where
336 S: crate::transport::ResponseStorageSanitizer + ?Sized,
337{
338 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
339 formatter
340 .debug_struct("CheckedResponseGuard")
341 .field("status", &self.status())
342 .field("body_len", &self.snapshot.body_len)
343 .field("body", &"[redacted]")
344 .field("content_type", &self.content_type())
345 .field("rate_limit", &self.rate_limit())
346 .finish()
347 }
348}
349
350impl fmt::Debug for CheckedResponse<'_> {
351 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
352 formatter
353 .debug_struct("CheckedResponse")
354 .field("status", &self.status())
355 .field("body_len", &self.body().len())
356 .field("body", &"[redacted]")
357 .field("content_type", &self.content_type())
358 .field("rate_limit", &self.rate_limit())
359 .finish()
360 }
361}
362
363fn validate_statuses(statuses: &[StatusCode]) -> Result<(), ResponsePolicyValidationError> {
364 if statuses.is_empty() {
365 return Err(ResponsePolicyValidationError::MissingSuccessStatus);
366 }
367 for (index, status) in statuses.iter().enumerate() {
368 if !status.is_success() {
369 return Err(ResponsePolicyValidationError::NonSuccessStatus);
370 }
371 if statuses
372 .get(..index)
373 .is_some_and(|seen| seen.contains(status))
374 {
375 return Err(ResponsePolicyValidationError::DuplicateSuccessStatus);
376 }
377 }
378 Ok(())
379}
380
381fn validate_media_types(policy: ContentTypePolicy) -> Result<(), ResponsePolicyValidationError> {
382 let media_types = match policy {
383 ContentTypePolicy::Required(values) | ContentTypePolicy::Optional(values) => values,
384 ContentTypePolicy::Forbidden => return Ok(()),
385 };
386 if media_types.is_empty() {
387 return Err(ResponsePolicyValidationError::MissingAcceptedMediaType);
388 }
389 for (index, media_type) in media_types.iter().enumerate() {
390 if media_types.get(..index).is_some_and(|seen| {
391 seen.iter()
392 .any(|candidate| candidate.as_str().eq_ignore_ascii_case(media_type.as_str()))
393 }) {
394 return Err(ResponsePolicyValidationError::DuplicateAcceptedMediaType);
395 }
396 }
397 Ok(())
398}
399
400fn validate_content_type(
401 policy: ContentTypePolicy,
402 actual: Option<ResponseContentType>,
403) -> Result<(), ResponsePolicyError> {
404 match (policy, actual) {
405 (ContentTypePolicy::Required(_), None) => Err(ResponsePolicyError::MissingContentType),
406 (ContentTypePolicy::Forbidden, Some(_)) => Err(ResponsePolicyError::ForbiddenContentType),
407 (ContentTypePolicy::Forbidden | ContentTypePolicy::Optional(_), None) => Ok(()),
408 (
409 ContentTypePolicy::Required(accepted) | ContentTypePolicy::Optional(accepted),
410 Some(actual),
411 ) => {
412 if accepted
413 .iter()
414 .any(|media_type| actual.matches(*media_type))
415 {
416 Ok(())
417 } else {
418 Err(ResponsePolicyError::UnexpectedContentType)
419 }
420 }
421 }
422}
423
424const fn map_writer_error(error: ResponseWriterError) -> ResponsePolicyError {
425 match error {
426 ResponseWriterError::NotCommitted
427 | ResponseWriterError::AlreadyCommitted
428 | ResponseWriterError::InitializedLengthTooLarge => {
429 ResponsePolicyError::UncommittedResponse
430 }
431 }
432}