Skip to main content

cloud_sdk/pagination/
header_cursor.rs

1use core::fmt;
2
3use cloud_sdk_sanitization::{SecretBuffer, sanitize_bytes};
4
5use super::{PaginationCursor, PaginationError, PaginationLimits};
6use crate::buffer::write_u64;
7use crate::operation::OperationId;
8use crate::transport::{
9    HeaderName, HeaderSensitivity, RequestHeader, RequestHeaders, ResponseHeaders,
10};
11
12mod execution;
13pub use execution::{
14    HeaderCursorContinuation, HeaderCursorExecutionError, HeaderCursorNext, HeaderCursorPage,
15    HeaderCursorSession,
16};
17
18/// Source-bound header names and page size for opaque cursor pagination.
19#[derive(Clone, Copy)]
20pub struct HeaderCursorPolicy<'a> {
21    operation: OperationId,
22    cursor_request: HeaderName<'a>,
23    size_request: HeaderName<'a>,
24    next_response: HeaderName<'a>,
25    page_size: u64,
26}
27
28impl<'a> HeaderCursorPolicy<'a> {
29    /// Creates a policy with three distinct HTTP header names and a nonzero size.
30    pub fn new(
31        operation: OperationId,
32        cursor_request: &'a str,
33        size_request: &'a str,
34        next_response: &'a str,
35        page_size: u64,
36    ) -> Result<Self, PaginationError> {
37        let cursor_request =
38            HeaderName::new(cursor_request).map_err(|_| PaginationError::InvalidHeaderPolicy)?;
39        let size_request =
40            HeaderName::new(size_request).map_err(|_| PaginationError::InvalidHeaderPolicy)?;
41        let next_response =
42            HeaderName::new(next_response).map_err(|_| PaginationError::InvalidHeaderPolicy)?;
43        RequestHeader::sensitive(cursor_request.as_str(), "cursor")
44            .map_err(|_| PaginationError::InvalidHeaderPolicy)?;
45        RequestHeader::new(size_request.as_str(), "1")
46            .map_err(|_| PaginationError::InvalidHeaderPolicy)?;
47        if page_size == 0 {
48            return Err(PaginationError::PageSizeZero);
49        }
50        if cursor_request == size_request
51            || cursor_request == next_response
52            || size_request == next_response
53        {
54            return Err(PaginationError::InvalidHeaderPolicy);
55        }
56        Ok(Self {
57            operation,
58            cursor_request,
59            size_request,
60            next_response,
61            page_size,
62        })
63    }
64
65    /// Returns the provider operation that owns every decoded cursor.
66    #[must_use]
67    pub const fn operation_id(self) -> OperationId {
68        self.operation
69    }
70
71    /// Returns the request cursor header name.
72    #[must_use]
73    pub const fn cursor_request(self) -> HeaderName<'a> {
74        self.cursor_request
75    }
76
77    /// Returns the request page-size header name.
78    #[must_use]
79    pub const fn size_request(self) -> HeaderName<'a> {
80        self.size_request
81    }
82
83    /// Returns the response next-cursor header name.
84    #[must_use]
85    pub const fn next_response(self) -> HeaderName<'a> {
86        self.next_response
87    }
88
89    /// Returns the fixed page size for this traversal.
90    #[must_use]
91    pub const fn page_size(self) -> u64 {
92        self.page_size
93    }
94
95    pub(super) fn with_request_headers<R>(
96        self,
97        cursor: Option<&PaginationCursor<'_>>,
98        decimal_scratch: &mut [u8],
99        inspect: impl FnOnce(RequestHeaders<'_>) -> R,
100    ) -> Result<R, PaginationError> {
101        let mut scratch = SecretBuffer::new(decimal_scratch);
102        let mut len = 0_usize;
103        write_u64(
104            scratch.as_mut_slice(),
105            &mut len,
106            self.page_size,
107            PaginationError::OutputTooSmall,
108        )?;
109        let size = core::str::from_utf8(
110            scratch
111                .as_slice()
112                .get(..len)
113                .ok_or(PaginationError::OutputTooSmall)?,
114        )
115        .map_err(|_| PaginationError::InvalidHeaderState)?;
116        match cursor {
117            None => self.inspect_request_headers(size, None, inspect),
118            Some(cursor) => cursor.with_cursor(|value| {
119                let value =
120                    core::str::from_utf8(value).map_err(|_| PaginationError::InvalidHeaderState)?;
121                self.inspect_request_headers(size, Some(value), inspect)
122            }),
123        }
124    }
125
126    pub(super) fn decode_next<'storage>(
127        self,
128        headers: &ResponseHeaders<'_>,
129        transfer_scratch: &mut [u8],
130        destination: &'storage mut [u8],
131        limits: PaginationLimits,
132    ) -> Result<DecodedHeaderCursor<'storage>, PaginationError> {
133        sanitize_bytes(transfer_scratch);
134        sanitize_bytes(destination);
135        let Some(header) = headers.get(self.next_response.as_str()) else {
136            return Ok(DecodedHeaderCursor::Complete);
137        };
138        if header.sensitivity() != HeaderSensitivity::Sensitive {
139            return Err(PaginationError::InsecureHeaderState);
140        }
141        let value = header.value();
142        validate_cursor_value(value, limits)?;
143        let source = transfer_scratch
144            .get_mut(..value.len())
145            .ok_or(PaginationError::OutputTooSmall)?;
146        source.copy_from_slice(value);
147        PaginationCursor::transfer_from(source, destination, limits)
148            .map(DecodedHeaderCursor::Continue)
149    }
150
151    fn inspect_request_headers<R>(
152        self,
153        size: &str,
154        cursor: Option<&str>,
155        inspect: impl FnOnce(RequestHeaders<'_>) -> R,
156    ) -> Result<R, PaginationError> {
157        let size = RequestHeader::new(self.size_request.as_str(), size)
158            .map_err(|_| PaginationError::InvalidHeaderState)?;
159        if let Some(cursor) = cursor {
160            let cursor = RequestHeader::sensitive(self.cursor_request.as_str(), cursor)
161                .map_err(|_| PaginationError::InvalidHeaderState)?;
162            let entries = [size, cursor];
163            let headers =
164                RequestHeaders::new(&entries).map_err(|_| PaginationError::InvalidHeaderPolicy)?;
165            return Ok(inspect(headers));
166        }
167        let entries = [size];
168        let headers =
169            RequestHeaders::new(&entries).map_err(|_| PaginationError::InvalidHeaderPolicy)?;
170        Ok(inspect(headers))
171    }
172}
173
174impl fmt::Debug for HeaderCursorPolicy<'_> {
175    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176        formatter
177            .debug_struct("HeaderCursorPolicy")
178            .field("operation", &self.operation)
179            .field("cursor_request", &self.cursor_request)
180            .field("size_request", &self.size_request)
181            .field("next_response", &self.next_response)
182            .field("page_size", &self.page_size)
183            .finish()
184    }
185}
186
187pub(super) enum DecodedHeaderCursor<'storage> {
188    Complete,
189    Continue(PaginationCursor<'storage>),
190}
191
192fn validate_cursor_value(value: &[u8], limits: PaginationLimits) -> Result<(), PaginationError> {
193    if value.is_empty() {
194        return Err(PaginationError::MissingState);
195    }
196    if value.len() > limits.max_state_bytes() {
197        return Err(PaginationError::StateTooLong);
198    }
199    if !value.iter().all(|byte| (b' '..=b'~').contains(byte))
200        || value.first() == Some(&b' ')
201        || value.last() == Some(&b' ')
202    {
203        return Err(PaginationError::InvalidHeaderState);
204    }
205    Ok(())
206}