1mod cursor;
3use super::RequestIdPolicy;
4use crate::rate_limit::RateLimit;
5use crate::transport::{
6 MediaType, ResponseBuffer, ResponseContentType, ResponseDecodeWorkspace, ResponseWriterError,
7 RetainedMetadataError, RetainedResponseMetadata, StatusCode, TransportResponse,
8};
9use core::fmt;
10#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12pub enum ResponseBodyPolicy {
13 Required,
15 Optional,
17 Forbidden,
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum ContentTypePolicy {
24 Required(&'static [MediaType<'static>]),
26 Optional(&'static [MediaType<'static>]),
28 Forbidden,
30}
31
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum ResponsePolicyValidationError {
35 MissingSuccessStatus,
37 NonSuccessStatus,
39 DuplicateSuccessStatus,
41 MissingAcceptedMediaType,
43 DuplicateAcceptedMediaType,
45 RequiredBodyHasZeroLimit,
47 ForbiddenBodyHasNonzeroLimit,
49 ForbiddenBodyAllowsContentType,
51 RequiredBodyForbidsContentType,
53}
54
55impl_static_error!(ResponsePolicyValidationError,
56 Self::MissingSuccessStatus => "response policy has no success status",
57 Self::NonSuccessStatus => "response policy contains a non-success status",
58 Self::DuplicateSuccessStatus => "response policy contains duplicate statuses",
59 Self::MissingAcceptedMediaType => "response policy has no accepted media type",
60 Self::DuplicateAcceptedMediaType => "response policy contains duplicate media types",
61 Self::RequiredBodyHasZeroLimit => "required response body has a zero limit",
62 Self::ForbiddenBodyHasNonzeroLimit => "forbidden response body has a nonzero limit",
63 Self::ForbiddenBodyAllowsContentType => "forbidden response body allows a content type",
64 Self::RequiredBodyForbidsContentType => "required response body forbids its content type",
65);
66
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub enum ResponsePolicyError {
70 UnexpectedStatus,
72 BodyTooLarge,
74 MissingBody,
76 ForbiddenBody,
78 MissingContentType,
80 InvalidContentType,
82 UnexpectedContentType,
84 ForbiddenContentType,
86 UncommittedResponse,
88 InvalidRequestId,
90}
91
92impl_static_error!(ResponsePolicyError,
93 Self::UnexpectedStatus => "response status is not expected",
94 Self::BodyTooLarge => "response body exceeds the operation limit",
95 Self::MissingBody => "required response body is missing",
96 Self::ForbiddenBody => "response body is forbidden",
97 Self::MissingContentType => "required response content type is missing",
98 Self::InvalidContentType => "response content type is invalid",
99 Self::UnexpectedContentType => "response content type is not accepted",
100 Self::ForbiddenContentType => "response content type is forbidden",
101 Self::UncommittedResponse => "response writer is not committed",
102 Self::InvalidRequestId => "response request identifier is invalid",
103);
104
105#[derive(Clone, Copy, Debug, Eq, PartialEq)]
107pub struct ResponsePolicy {
108 success_statuses: &'static [StatusCode],
109 content_type: ContentTypePolicy,
110 body: ResponseBodyPolicy,
111 max_body_bytes: usize,
112}
113
114impl ResponsePolicy {
115 pub fn new(
117 success_statuses: &'static [StatusCode],
118 content_type: ContentTypePolicy,
119 body: ResponseBodyPolicy,
120 max_body_bytes: usize,
121 ) -> Result<Self, ResponsePolicyValidationError> {
122 validate_statuses(success_statuses)?;
123 validate_media_types(content_type)?;
124 match (body, content_type, max_body_bytes) {
125 (ResponseBodyPolicy::Required, _, 0) => {
126 return Err(ResponsePolicyValidationError::RequiredBodyHasZeroLimit);
127 }
128 (ResponseBodyPolicy::Forbidden, _, limit) if limit != 0 => {
129 return Err(ResponsePolicyValidationError::ForbiddenBodyHasNonzeroLimit);
130 }
131 (
132 ResponseBodyPolicy::Forbidden,
133 ContentTypePolicy::Required(_) | ContentTypePolicy::Optional(_),
134 _,
135 ) => {
136 return Err(ResponsePolicyValidationError::ForbiddenBodyAllowsContentType);
137 }
138 (ResponseBodyPolicy::Required, ContentTypePolicy::Forbidden, _) => {
139 return Err(ResponsePolicyValidationError::RequiredBodyForbidsContentType);
140 }
141 _ => {}
142 }
143 Ok(Self {
144 success_statuses,
145 content_type,
146 body,
147 max_body_bytes,
148 })
149 }
150
151 #[must_use]
153 pub const fn success_statuses(self) -> &'static [StatusCode] {
154 self.success_statuses
155 }
156 #[must_use]
158 pub const fn content_type_policy(self) -> ContentTypePolicy {
159 self.content_type
160 }
161 #[must_use]
163 pub const fn body_policy(self) -> ResponseBodyPolicy {
164 self.body
165 }
166
167 #[must_use]
169 pub const fn max_body_bytes(self) -> usize {
170 self.max_body_bytes
171 }
172
173 pub fn validate<'buffer>(
175 self,
176 mut writer: ResponseBuffer<'buffer>,
177 request_id_policy: RequestIdPolicy,
178 ) -> Result<CheckedResponseGuard<'buffer>, ResponsePolicyError> {
179 apply_request_id_policy(&mut writer, request_id_policy)?;
180 let snapshot = {
181 let response = writer.response().map_err(map_writer_error)?;
182 self.validate_view(response, request_id_policy)?
183 };
184 Ok(CheckedResponseGuard {
185 writer,
186 snapshot,
187 workspace: ResponseDecodeWorkspace::new(),
188 })
189 }
190
191 fn validate_view(
192 self,
193 response: TransportResponse<'_, '_>,
194 request_id_policy: RequestIdPolicy,
195 ) -> Result<CheckedResponseSnapshot, ResponsePolicyError> {
196 if !self.success_statuses.contains(&response.status()) {
197 return Err(ResponsePolicyError::UnexpectedStatus);
198 }
199 match self.body {
200 ResponseBodyPolicy::Forbidden if !response.body().is_empty() => {
201 return Err(ResponsePolicyError::ForbiddenBody);
202 }
203 _ => {}
204 }
205 if response.body().len() > self.max_body_bytes {
206 return Err(ResponsePolicyError::BodyTooLarge);
207 }
208 if matches!(self.body, ResponseBodyPolicy::Required) && response.body().is_empty() {
209 return Err(ResponsePolicyError::MissingBody);
210 }
211 let content_type = response
212 .content_type()
213 .map_err(|_| ResponsePolicyError::InvalidContentType)?;
214 validate_content_type(self.content_type, content_type)?;
215 Ok(CheckedResponseSnapshot {
216 status: response.status(),
217 body_len: response.body().len(),
218 rate_limit: response.rate_limit(),
219 request_id_policy,
220 })
221 }
222}
223
224pub(crate) fn apply_request_id_policy(
225 writer: &mut ResponseBuffer<'_>,
226 request_id_policy: RequestIdPolicy,
227) -> Result<(), ResponsePolicyError> {
228 writer.response().map_err(map_writer_error)?;
229 writer
230 .apply_request_id_policy(request_id_policy)
231 .map_err(|_| ResponsePolicyError::InvalidRequestId)
232}
233
234#[derive(Clone, Copy)]
236pub struct CheckedResponse<'body> {
237 status: StatusCode,
238 body: &'body [u8],
239 headers: &'body crate::transport::ResponseHeaders<'body>,
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
400fn checked_response<'response>(
401 writer: &'response ResponseBuffer<'_>,
402 snapshot: CheckedResponseSnapshot,
403) -> CheckedResponse<'response> {
404 CheckedResponse {
405 status: snapshot.status,
406 body: writer.initialized_body(snapshot.body_len),
407 headers: writer.headers(),
408 content_type: writer
409 .response()
410 .ok()
411 .and_then(|response| response.content_type().ok().flatten()),
412 rate_limit: snapshot.rate_limit,
413 request_id: writer.request_id(),
414 request_id_policy: snapshot.request_id_policy,
415 }
416}
417
418fn validate_statuses(statuses: &[StatusCode]) -> Result<(), ResponsePolicyValidationError> {
419 if statuses.is_empty() {
420 return Err(ResponsePolicyValidationError::MissingSuccessStatus);
421 }
422 for (index, status) in statuses.iter().enumerate() {
423 if !status.is_success() {
424 return Err(ResponsePolicyValidationError::NonSuccessStatus);
425 }
426 if statuses
427 .get(..index)
428 .is_some_and(|seen| seen.contains(status))
429 {
430 return Err(ResponsePolicyValidationError::DuplicateSuccessStatus);
431 }
432 }
433 Ok(())
434}
435
436fn validate_media_types(policy: ContentTypePolicy) -> Result<(), ResponsePolicyValidationError> {
437 let media_types = match policy {
438 ContentTypePolicy::Required(values) | ContentTypePolicy::Optional(values) => values,
439 ContentTypePolicy::Forbidden => return Ok(()),
440 };
441 if media_types.is_empty() {
442 return Err(ResponsePolicyValidationError::MissingAcceptedMediaType);
443 }
444 for (index, media_type) in media_types.iter().enumerate() {
445 if media_types.get(..index).is_some_and(|seen| {
446 seen.iter()
447 .any(|candidate| candidate.as_str().eq_ignore_ascii_case(media_type.as_str()))
448 }) {
449 return Err(ResponsePolicyValidationError::DuplicateAcceptedMediaType);
450 }
451 }
452 Ok(())
453}
454
455fn validate_content_type(
456 policy: ContentTypePolicy,
457 actual: Option<ResponseContentType<'_>>,
458) -> Result<(), ResponsePolicyError> {
459 match (policy, actual) {
460 (ContentTypePolicy::Required(_), None) => Err(ResponsePolicyError::MissingContentType),
461 (ContentTypePolicy::Forbidden, Some(_)) => Err(ResponsePolicyError::ForbiddenContentType),
462 (ContentTypePolicy::Forbidden | ContentTypePolicy::Optional(_), None) => Ok(()),
463 (
464 ContentTypePolicy::Required(accepted) | ContentTypePolicy::Optional(accepted),
465 Some(actual),
466 ) => {
467 if accepted
468 .iter()
469 .any(|media_type| actual.matches(*media_type))
470 {
471 Ok(())
472 } else {
473 Err(ResponsePolicyError::UnexpectedContentType)
474 }
475 }
476 }
477}
478const fn map_writer_error(error: ResponseWriterError) -> ResponsePolicyError {
479 match error {
480 ResponseWriterError::NotCommitted
481 | ResponseWriterError::AlreadyCommitted
482 | ResponseWriterError::InitializedLengthTooLarge => {
483 ResponsePolicyError::UncommittedResponse
484 }
485 }
486}