Skip to main content

cloud_sdk/pagination/
opaque.rs

1use core::fmt;
2
3use cloud_sdk_sanitization::{SecretBuffer, sanitize_bytes, sanitize_value};
4
5use super::{PaginationError, PaginationLimits};
6
7/// Maximum opaque cursor, marker, or provider-link target length.
8pub const MAX_OPAQUE_STATE_BYTES: usize = 8192;
9const DIGEST_BYTES: usize = 32;
10const HISTORY_PREFIX_BYTES: usize = 2 + DIGEST_BYTES;
11
12struct OpaqueState<'storage> {
13    bytes: SecretBuffer<'storage>,
14    len: usize,
15}
16
17impl<'storage> OpaqueState<'storage> {
18    fn transfer_from(
19        source: &mut [u8],
20        destination: &'storage mut [u8],
21        limits: PaginationLimits,
22    ) -> Result<Self, PaginationError> {
23        sanitize_bytes(destination);
24        let source = SecretBuffer::new(source);
25        let mut destination = SecretBuffer::new(destination);
26        let value = source.as_slice();
27        if value.is_empty() {
28            return Err(PaginationError::MissingState);
29        }
30        if value.len() > limits.max_state_bytes() {
31            return Err(PaginationError::StateTooLong);
32        }
33        let output = destination
34            .as_mut_slice()
35            .get_mut(..value.len())
36            .ok_or(PaginationError::OutputTooSmall)?;
37        output.copy_from_slice(value);
38        Ok(Self {
39            bytes: destination,
40            len: value.len(),
41        })
42    }
43
44    fn with_bytes<R>(&self, inspect: impl FnOnce(&[u8]) -> R) -> R {
45        let value = self.bytes.as_slice().get(..self.len).unwrap_or_default();
46        inspect(value)
47    }
48}
49
50impl Drop for OpaqueState<'_> {
51    fn drop(&mut self) {
52        sanitize_value(&mut self.len);
53    }
54}
55
56/// Cleanup-owning opaque cursor populated by atomic source transfer.
57///
58/// This type is intentionally neither `Copy` nor `Clone`.
59///
60/// ```compile_fail
61/// use cloud_sdk::pagination::PaginationCursor;
62/// fn require_copy<T: Copy>() {}
63/// require_copy::<PaginationCursor<'static>>();
64/// ```
65pub struct PaginationCursor<'storage> {
66    state: OpaqueState<'storage>,
67}
68
69impl<'storage> PaginationCursor<'storage> {
70    /// Moves source bytes into cleanup-owning caller storage.
71    ///
72    /// Source and complete destination storage are cleared on every failure.
73    /// Source is also cleared after a successful transfer.
74    pub fn transfer_from(
75        source: &mut [u8],
76        destination: &'storage mut [u8],
77        limits: PaginationLimits,
78    ) -> Result<Self, PaginationError> {
79        OpaqueState::transfer_from(source, destination, limits).map(|state| Self { state })
80    }
81
82    /// Runs a closure with the exact opaque cursor bytes.
83    pub fn with_cursor<R>(&self, inspect: impl FnOnce(&[u8]) -> R) -> R {
84        self.state.with_bytes(inspect)
85    }
86}
87
88impl fmt::Debug for PaginationCursor<'_> {
89    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90        formatter.write_str("PaginationCursor([redacted])")
91    }
92}
93
94/// Cleanup-owning opaque marker populated by atomic source transfer.
95///
96/// This type is intentionally neither `Copy` nor `Clone`.
97///
98/// ```compile_fail
99/// use cloud_sdk::pagination::PaginationMarker;
100/// fn require_copy<T: Copy>() {}
101/// require_copy::<PaginationMarker<'static>>();
102/// ```
103pub struct PaginationMarker<'storage> {
104    state: OpaqueState<'storage>,
105}
106
107impl<'storage> PaginationMarker<'storage> {
108    /// Moves source bytes into cleanup-owning caller storage.
109    pub fn transfer_from(
110        source: &mut [u8],
111        destination: &'storage mut [u8],
112        limits: PaginationLimits,
113    ) -> Result<Self, PaginationError> {
114        OpaqueState::transfer_from(source, destination, limits).map(|state| Self { state })
115    }
116
117    /// Runs a closure with the exact opaque marker bytes.
118    pub fn with_marker<R>(&self, inspect: impl FnOnce(&[u8]) -> R) -> R {
119        self.state.with_bytes(inspect)
120    }
121}
122
123impl fmt::Debug for PaginationMarker<'_> {
124    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125        formatter.write_str("PaginationMarker([redacted])")
126    }
127}
128
129/// Caller-produced fixed-size cursor digest.
130#[derive(Clone, Copy, Eq, PartialEq)]
131pub struct CursorDigest([u8; DIGEST_BYTES]);
132
133impl CursorDigest {
134    /// Wraps a digest produced by the caller-selected digest implementation.
135    #[must_use]
136    pub const fn new(value: [u8; DIGEST_BYTES]) -> Self {
137        Self(value)
138    }
139}
140
141impl fmt::Debug for CursorDigest {
142    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
143        formatter.write_str("CursorDigest([redacted])")
144    }
145}
146
147/// Cleanup-owning exact cursor history used for cycle and collision checks.
148///
149/// Each entry stores both the caller digest and exact cursor bytes. Equal
150/// digests with unequal bytes fail as collisions; equal bytes with another
151/// digest also fail closed. This type is intentionally neither `Copy` nor
152/// `Clone`.
153pub struct CursorHistory<'storage> {
154    bytes: SecretBuffer<'storage>,
155    used: usize,
156    entries: u32,
157    max_entries: u32,
158}
159
160impl<'storage> CursorHistory<'storage> {
161    /// Creates empty history in caller-owned cleanup storage.
162    pub fn new(storage: &'storage mut [u8], max_entries: u32) -> Result<Self, PaginationError> {
163        sanitize_bytes(storage);
164        if max_entries == 0 {
165            return Err(PaginationError::ZeroLimit);
166        }
167        Ok(Self {
168            bytes: SecretBuffer::new(storage),
169            used: 0,
170            entries: 0,
171            max_entries,
172        })
173    }
174
175    /// Checks and transactionally records one cursor and digest.
176    pub fn observe(
177        &mut self,
178        cursor: &PaginationCursor<'_>,
179        digest: CursorDigest,
180    ) -> Result<(), PaginationError> {
181        cursor
182            .state
183            .with_bytes(|state| self.observe_bytes(state, digest))
184    }
185
186    /// Returns the number of recorded cursors.
187    #[must_use]
188    pub const fn entries(&self) -> u32 {
189        self.entries
190    }
191
192    fn observe_bytes(&mut self, state: &[u8], digest: CursorDigest) -> Result<(), PaginationError> {
193        let mut position = 0_usize;
194        while position < self.used {
195            let prefix_end = position
196                .checked_add(HISTORY_PREFIX_BYTES)
197                .ok_or(PaginationError::HistoryBudgetExceeded)?;
198            let prefix = self
199                .bytes
200                .as_slice()
201                .get(position..prefix_end)
202                .ok_or(PaginationError::HistoryBudgetExceeded)?;
203            let state_len_bytes: [u8; 2] = prefix
204                .get(..2)
205                .ok_or(PaginationError::HistoryBudgetExceeded)?
206                .try_into()
207                .map_err(|_| PaginationError::HistoryBudgetExceeded)?;
208            let state_len = usize::from(u16::from_be_bytes(state_len_bytes));
209            let stored_digest = prefix
210                .get(2..HISTORY_PREFIX_BYTES)
211                .ok_or(PaginationError::HistoryBudgetExceeded)?;
212            let state_end = prefix_end
213                .checked_add(state_len)
214                .ok_or(PaginationError::HistoryBudgetExceeded)?;
215            let stored_state = self
216                .bytes
217                .as_slice()
218                .get(prefix_end..state_end)
219                .ok_or(PaginationError::HistoryBudgetExceeded)?;
220            if stored_digest == digest.0 {
221                return if stored_state == state {
222                    Err(PaginationError::CursorCycle)
223                } else {
224                    Err(PaginationError::CursorDigestCollision)
225                };
226            }
227            if stored_state == state {
228                return Err(PaginationError::CursorDigestChanged);
229            }
230            position = state_end;
231        }
232        if self.entries >= self.max_entries {
233            return Err(PaginationError::HistoryBudgetExceeded);
234        }
235        let state_len =
236            u16::try_from(state.len()).map_err(|_| PaginationError::HistoryBudgetExceeded)?;
237        let next_used = self
238            .used
239            .checked_add(HISTORY_PREFIX_BYTES)
240            .and_then(|value| value.checked_add(state.len()))
241            .ok_or(PaginationError::HistoryBudgetExceeded)?;
242        let output = self
243            .bytes
244            .as_mut_slice()
245            .get_mut(self.used..next_used)
246            .ok_or(PaginationError::HistoryBudgetExceeded)?;
247        output
248            .get_mut(..2)
249            .ok_or(PaginationError::HistoryBudgetExceeded)?
250            .copy_from_slice(&state_len.to_be_bytes());
251        output
252            .get_mut(2..HISTORY_PREFIX_BYTES)
253            .ok_or(PaginationError::HistoryBudgetExceeded)?
254            .copy_from_slice(&digest.0);
255        output
256            .get_mut(HISTORY_PREFIX_BYTES..)
257            .ok_or(PaginationError::HistoryBudgetExceeded)?
258            .copy_from_slice(state);
259        self.used = next_used;
260        self.entries = self
261            .entries
262            .checked_add(1)
263            .ok_or(PaginationError::HistoryBudgetExceeded)?;
264        Ok(())
265    }
266}
267
268impl Drop for CursorHistory<'_> {
269    fn drop(&mut self) {
270        sanitize_value(&mut self.used);
271        sanitize_value(&mut self.entries);
272    }
273}
274
275impl fmt::Debug for CursorHistory<'_> {
276    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
277        formatter
278            .debug_struct("CursorHistory")
279            .field("entries", &self.entries)
280            .field("state", &"[redacted]")
281            .finish()
282    }
283}