1mod cursor;
3use core::fmt;
4
5use super::RequestIdPolicy;
6use crate::rate_limit::RateLimit;
7use crate::transport::{
8 MediaType, ResponseBuffer, ResponseContentType, ResponseDecodeWorkspace, ResponseWriterError,
9 RetainedMetadataError, RetainedResponseMetadata, StatusCode, TransportResponse,
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 InvalidContentType,
83 UnexpectedContentType,
85 ForbiddenContentType,
87 UncommittedResponse,
89 InvalidRequestId,
91}
92
93impl_static_error!(ResponsePolicyError,
94 Self::UnexpectedStatus => "response status is not expected",
95 Self::BodyTooLarge => "response body exceeds the operation limit",
96 Self::MissingBody => "required response body is missing",
97 Self::ForbiddenBody => "response body is forbidden",
98 Self::MissingContentType => "required response content type is missing",
99 Self::InvalidContentType => "response content type is invalid",
100 Self::UnexpectedContentType => "response content type is not accepted",
101 Self::ForbiddenContentType => "response content type is forbidden",
102 Self::UncommittedResponse => "response writer is not committed",
103 Self::InvalidRequestId => "response request identifier is invalid",
104);
105
106#[derive(Clone, Copy, Debug, Eq, PartialEq)]
108pub struct ResponsePolicy {
109 success_statuses: &'static [StatusCode],
110 content_type: ContentTypePolicy,
111 body: ResponseBodyPolicy,
112 max_body_bytes: usize,
113}
114
115impl ResponsePolicy {
116 pub fn new(
118 success_statuses: &'static [StatusCode],
119 content_type: ContentTypePolicy,
120 body: ResponseBodyPolicy,
121 max_body_bytes: usize,
122 ) -> Result<Self, ResponsePolicyValidationError> {
123 validate_statuses(success_statuses)?;
124 validate_media_types(content_type)?;
125 match (body, content_type, max_body_bytes) {
126 (ResponseBodyPolicy::Required, _, 0) => {
127 return Err(ResponsePolicyValidationError::RequiredBodyHasZeroLimit);
128 }
129 (ResponseBodyPolicy::Forbidden, _, limit) if limit != 0 => {
130 return Err(ResponsePolicyValidationError::ForbiddenBodyHasNonzeroLimit);
131 }
132 (
133 ResponseBodyPolicy::Forbidden,
134 ContentTypePolicy::Required(_) | ContentTypePolicy::Optional(_),
135 _,
136 ) => {
137 return Err(ResponsePolicyValidationError::ForbiddenBodyAllowsContentType);
138 }
139 (ResponseBodyPolicy::Required, ContentTypePolicy::Forbidden, _) => {
140 return Err(ResponsePolicyValidationError::RequiredBodyForbidsContentType);
141 }
142 _ => {}
143 }
144 Ok(Self {
145 success_statuses,
146 content_type,
147 body,
148 max_body_bytes,
149 })
150 }
151
152 #[must_use]
154 pub const fn success_statuses(self) -> &'static [StatusCode] {
155 self.success_statuses
156 }
157 #[must_use]
159 pub const fn content_type_policy(self) -> ContentTypePolicy {
160 self.content_type
161 }
162 #[must_use]
164 pub const fn body_policy(self) -> ResponseBodyPolicy {
165 self.body
166 }
167
168 #[must_use]
170 pub const fn max_body_bytes(self) -> usize {
171 self.max_body_bytes
172 }
173
174 pub fn validate<'buffer>(
176 self,
177 mut writer: ResponseBuffer<'buffer>,
178 request_id_policy: RequestIdPolicy,
179 ) -> Result<CheckedResponseGuard<'buffer>, ResponsePolicyError> {
180 apply_request_id_policy(&mut writer, request_id_policy)?;
181 let snapshot = {
182 let response = writer.response().map_err(map_writer_error)?;
183 self.validate_view(response, request_id_policy)?
184 };
185 Ok(CheckedResponseGuard {
186 writer,
187 snapshot,
188 workspace: ResponseDecodeWorkspace::new(),
189 })
190 }
191
192 fn validate_view(
193 self,
194 response: TransportResponse<'_, '_>,
195 request_id_policy: RequestIdPolicy,
196 ) -> Result<CheckedResponseSnapshot, ResponsePolicyError> {
197 if !self.success_statuses.contains(&response.status()) {
198 return Err(ResponsePolicyError::UnexpectedStatus);
199 }
200 match self.body {
201 ResponseBodyPolicy::Forbidden if !response.body().is_empty() => {
202 return Err(ResponsePolicyError::ForbiddenBody);
203 }
204 _ => {}
205 }
206 if response.body().len() > self.max_body_bytes {
207 return Err(ResponsePolicyError::BodyTooLarge);
208 }
209 if matches!(self.body, ResponseBodyPolicy::Required) && response.body().is_empty() {
210 return Err(ResponsePolicyError::MissingBody);
211 }
212 let content_type = response
213 .content_type()
214 .map_err(|_| ResponsePolicyError::InvalidContentType)?;
215 validate_content_type(self.content_type, content_type)?;
216 Ok(CheckedResponseSnapshot {
217 status: response.status(),
218 body_len: response.body().len(),
219 rate_limit: response.rate_limit(),
220 request_id_policy,
221 })
222 }
223}
224
225pub(crate) fn apply_request_id_policy(
226 writer: &mut ResponseBuffer<'_>,
227 request_id_policy: RequestIdPolicy,
228) -> Result<(), ResponsePolicyError> {
229 writer.response().map_err(map_writer_error)?;
230 writer
231 .apply_request_id_policy(request_id_policy)
232 .map_err(|_| ResponsePolicyError::InvalidRequestId)
233}
234
235#[derive(Clone, Copy)]
237pub struct CheckedResponse<'body> {
238 status: StatusCode,
239 body: &'body [u8],
240 content_type: Option<ResponseContentType<'body>>,
241 rate_limit: Option<RateLimit>,
242 request_id: Option<&'body [u8]>,
243 request_id_policy: RequestIdPolicy,
244}
245
246impl<'body> CheckedResponse<'body> {
247 #[must_use]
249 pub const fn status(&self) -> StatusCode {
250 self.status
251 }
252
253 #[must_use]
255 pub const fn body(&self) -> &[u8] {
256 self.body
257 }
258
259 #[must_use]
261 pub const fn content_type(&self) -> Option<ResponseContentType<'body>> {
262 self.content_type
263 }
264
265 #[must_use]
267 pub const fn rate_limit(&self) -> Option<RateLimit> {
268 self.rate_limit
269 }
270
271 #[must_use]
273 pub const fn request_id_policy(&self) -> RequestIdPolicy {
274 self.request_id_policy
275 }
276
277 pub fn with_request_id<R>(&self, inspect: impl FnOnce(Option<&[u8]>) -> R) -> R {
279 inspect(self.request_id)
280 }
281}
282
283#[derive(Clone, Copy)]
284struct CheckedResponseSnapshot {
285 status: StatusCode,
286 body_len: usize,
287 rate_limit: Option<RateLimit>,
288 request_id_policy: RequestIdPolicy,
289}
290
291pub struct CheckedResponseGuard<'buffer> {
296 writer: ResponseBuffer<'buffer>,
297 snapshot: CheckedResponseSnapshot,
298 workspace: ResponseDecodeWorkspace,
299}
300
301impl CheckedResponseGuard<'_> {
302 #[must_use]
304 pub const fn status(&self) -> StatusCode {
305 self.snapshot.status
306 }
307
308 #[must_use]
310 pub fn content_type(&self) -> Option<ResponseContentType<'_>> {
311 self.writer
312 .response()
313 .ok()
314 .and_then(|response| response.content_type().ok().flatten())
315 }
316
317 #[must_use]
319 pub const fn rate_limit(&self) -> Option<RateLimit> {
320 self.snapshot.rate_limit
321 }
322
323 pub fn with_borrowed<R>(
334 &self,
335 inspect: impl for<'response> FnOnce(CheckedResponse<'response>) -> R,
336 ) -> R {
337 inspect(self.checked_response())
338 }
339
340 pub fn decode_owned<R, E>(
342 self,
343 decode: impl for<'response> FnOnce(CheckedResponse<'response>) -> Result<R, E>,
344 ) -> Result<R, E> {
345 self.decode_owned_with_workspace(|response, _workspace| decode(response))
346 }
347
348 pub fn decode_owned_with_workspace<R, E>(
350 mut self,
351 decode: impl for<'response> FnOnce(
352 CheckedResponse<'response>,
353 &mut ResponseDecodeWorkspace,
354 ) -> Result<R, E>,
355 ) -> Result<R, E> {
356 let result = {
357 let Self {
358 writer,
359 snapshot,
360 workspace,
361 } = &mut self;
362 let response = checked_response(writer, *snapshot);
363 decode(response, workspace)
364 };
365 drop(self);
366 result
367 }
368
369 pub fn retain_metadata_into<'destination>(
371 &mut self,
372 destination: &'destination mut [u8],
373 request_id_limit: usize,
374 ) -> Result<RetainedResponseMetadata<'destination>, RetainedMetadataError> {
375 if self.snapshot.request_id_policy != RequestIdPolicy::Retain {
376 return Err(RetainedMetadataError::RetentionForbidden);
377 }
378 self.writer.retain_request_id(destination, request_id_limit)
379 }
380
381 fn checked_response(&self) -> CheckedResponse<'_> {
382 checked_response(&self.writer, self.snapshot)
383 }
384}
385
386impl fmt::Debug for CheckedResponseGuard<'_> {
387 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
388 formatter
389 .debug_struct("CheckedResponseGuard")
390 .field("status", &self.status())
391 .field("body_len", &self.snapshot.body_len)
392 .field("body", &"[redacted]")
393 .field("content_type", &self.content_type())
394 .field("rate_limit", &self.rate_limit())
395 .field("request_id", &"[redacted]")
396 .finish()
397 }
398}
399
400impl fmt::Debug for CheckedResponse<'_> {
401 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
402 formatter
403 .debug_struct("CheckedResponse")
404 .field("status", &self.status())
405 .field("body_len", &self.body().len())
406 .field("body", &"[redacted]")
407 .field("content_type", &self.content_type())
408 .field("rate_limit", &self.rate_limit())
409 .field("request_id", &"[redacted]")
410 .finish()
411 }
412}
413
414fn checked_response<'response>(
415 writer: &'response ResponseBuffer<'_>,
416 snapshot: CheckedResponseSnapshot,
417) -> CheckedResponse<'response> {
418 CheckedResponse {
419 status: snapshot.status,
420 body: writer.initialized_body(snapshot.body_len),
421 content_type: writer
422 .response()
423 .ok()
424 .and_then(|response| response.content_type().ok().flatten()),
425 rate_limit: snapshot.rate_limit,
426 request_id: writer.request_id(),
427 request_id_policy: snapshot.request_id_policy,
428 }
429}
430
431fn validate_statuses(statuses: &[StatusCode]) -> Result<(), ResponsePolicyValidationError> {
432 if statuses.is_empty() {
433 return Err(ResponsePolicyValidationError::MissingSuccessStatus);
434 }
435 for (index, status) in statuses.iter().enumerate() {
436 if !status.is_success() {
437 return Err(ResponsePolicyValidationError::NonSuccessStatus);
438 }
439 if statuses
440 .get(..index)
441 .is_some_and(|seen| seen.contains(status))
442 {
443 return Err(ResponsePolicyValidationError::DuplicateSuccessStatus);
444 }
445 }
446 Ok(())
447}
448
449fn validate_media_types(policy: ContentTypePolicy) -> Result<(), ResponsePolicyValidationError> {
450 let media_types = match policy {
451 ContentTypePolicy::Required(values) | ContentTypePolicy::Optional(values) => values,
452 ContentTypePolicy::Forbidden => return Ok(()),
453 };
454 if media_types.is_empty() {
455 return Err(ResponsePolicyValidationError::MissingAcceptedMediaType);
456 }
457 for (index, media_type) in media_types.iter().enumerate() {
458 if media_types.get(..index).is_some_and(|seen| {
459 seen.iter()
460 .any(|candidate| candidate.as_str().eq_ignore_ascii_case(media_type.as_str()))
461 }) {
462 return Err(ResponsePolicyValidationError::DuplicateAcceptedMediaType);
463 }
464 }
465 Ok(())
466}
467
468fn validate_content_type(
469 policy: ContentTypePolicy,
470 actual: Option<ResponseContentType<'_>>,
471) -> Result<(), ResponsePolicyError> {
472 match (policy, actual) {
473 (ContentTypePolicy::Required(_), None) => Err(ResponsePolicyError::MissingContentType),
474 (ContentTypePolicy::Forbidden, Some(_)) => Err(ResponsePolicyError::ForbiddenContentType),
475 (ContentTypePolicy::Forbidden | ContentTypePolicy::Optional(_), None) => Ok(()),
476 (
477 ContentTypePolicy::Required(accepted) | ContentTypePolicy::Optional(accepted),
478 Some(actual),
479 ) => {
480 if accepted
481 .iter()
482 .any(|media_type| actual.matches(*media_type))
483 {
484 Ok(())
485 } else {
486 Err(ResponsePolicyError::UnexpectedContentType)
487 }
488 }
489 }
490}
491const fn map_writer_error(error: ResponseWriterError) -> ResponsePolicyError {
492 match error {
493 ResponseWriterError::NotCommitted
494 | ResponseWriterError::AlreadyCommitted
495 | ResponseWriterError::InitializedLengthTooLarge => {
496 ResponsePolicyError::UncommittedResponse
497 }
498 }
499}