cloud_sdk/pagination/
opaque.rs1use core::fmt;
2
3use cloud_sdk_sanitization::{SecretBuffer, sanitize_bytes, sanitize_value};
4
5use super::{PaginationError, PaginationLimits};
6
7pub 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
56pub struct PaginationCursor<'storage> {
66 state: OpaqueState<'storage>,
67}
68
69impl<'storage> PaginationCursor<'storage> {
70 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 pub fn with_cursor<R>(&self, inspect: impl FnOnce(&[u8]) -> R) -> R {
84 self.state.with_bytes(inspect)
85 }
86
87 pub(super) fn as_bytes(&self) -> &[u8] {
88 self.state
89 .bytes
90 .as_slice()
91 .get(..self.state.len)
92 .unwrap_or_default()
93 }
94}
95
96impl fmt::Debug for PaginationCursor<'_> {
97 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
98 formatter.write_str("PaginationCursor([redacted])")
99 }
100}
101
102pub struct PaginationMarker<'storage> {
112 state: OpaqueState<'storage>,
113}
114
115impl<'storage> PaginationMarker<'storage> {
116 pub fn transfer_from(
118 source: &mut [u8],
119 destination: &'storage mut [u8],
120 limits: PaginationLimits,
121 ) -> Result<Self, PaginationError> {
122 OpaqueState::transfer_from(source, destination, limits).map(|state| Self { state })
123 }
124
125 pub fn with_marker<R>(&self, inspect: impl FnOnce(&[u8]) -> R) -> R {
127 self.state.with_bytes(inspect)
128 }
129}
130
131impl fmt::Debug for PaginationMarker<'_> {
132 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
133 formatter.write_str("PaginationMarker([redacted])")
134 }
135}
136
137#[derive(Clone, Copy, Eq, PartialEq)]
139pub struct CursorDigest([u8; DIGEST_BYTES]);
140
141impl CursorDigest {
142 #[must_use]
144 pub const fn new(value: [u8; DIGEST_BYTES]) -> Self {
145 Self(value)
146 }
147}
148
149impl fmt::Debug for CursorDigest {
150 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
151 formatter.write_str("CursorDigest([redacted])")
152 }
153}
154
155pub struct CursorHistory<'storage> {
162 bytes: SecretBuffer<'storage>,
163 used: usize,
164 entries: u32,
165 max_entries: u32,
166}
167
168impl<'storage> CursorHistory<'storage> {
169 pub fn new(storage: &'storage mut [u8], max_entries: u32) -> Result<Self, PaginationError> {
171 sanitize_bytes(storage);
172 if max_entries == 0 {
173 return Err(PaginationError::ZeroLimit);
174 }
175 Ok(Self {
176 bytes: SecretBuffer::new(storage),
177 used: 0,
178 entries: 0,
179 max_entries,
180 })
181 }
182
183 pub fn observe(
185 &mut self,
186 cursor: &PaginationCursor<'_>,
187 digest: CursorDigest,
188 ) -> Result<(), PaginationError> {
189 cursor
190 .state
191 .with_bytes(|state| self.observe_bytes(state, digest))
192 }
193
194 #[must_use]
196 pub const fn entries(&self) -> u32 {
197 self.entries
198 }
199
200 fn observe_bytes(&mut self, state: &[u8], digest: CursorDigest) -> Result<(), PaginationError> {
201 let mut position = 0_usize;
202 while position < self.used {
203 let prefix_end = position
204 .checked_add(HISTORY_PREFIX_BYTES)
205 .ok_or(PaginationError::HistoryBudgetExceeded)?;
206 let prefix = self
207 .bytes
208 .as_slice()
209 .get(position..prefix_end)
210 .ok_or(PaginationError::HistoryBudgetExceeded)?;
211 let state_len_bytes: [u8; 2] = prefix
212 .get(..2)
213 .ok_or(PaginationError::HistoryBudgetExceeded)?
214 .try_into()
215 .map_err(|_| PaginationError::HistoryBudgetExceeded)?;
216 let state_len = usize::from(u16::from_be_bytes(state_len_bytes));
217 let stored_digest = prefix
218 .get(2..HISTORY_PREFIX_BYTES)
219 .ok_or(PaginationError::HistoryBudgetExceeded)?;
220 let state_end = prefix_end
221 .checked_add(state_len)
222 .ok_or(PaginationError::HistoryBudgetExceeded)?;
223 let stored_state = self
224 .bytes
225 .as_slice()
226 .get(prefix_end..state_end)
227 .ok_or(PaginationError::HistoryBudgetExceeded)?;
228 if stored_digest == digest.0 {
229 return if stored_state == state {
230 Err(PaginationError::CursorCycle)
231 } else {
232 Err(PaginationError::CursorDigestCollision)
233 };
234 }
235 if stored_state == state {
236 return Err(PaginationError::CursorDigestChanged);
237 }
238 position = state_end;
239 }
240 if self.entries >= self.max_entries {
241 return Err(PaginationError::HistoryBudgetExceeded);
242 }
243 let state_len =
244 u16::try_from(state.len()).map_err(|_| PaginationError::HistoryBudgetExceeded)?;
245 let next_used = self
246 .used
247 .checked_add(HISTORY_PREFIX_BYTES)
248 .and_then(|value| value.checked_add(state.len()))
249 .ok_or(PaginationError::HistoryBudgetExceeded)?;
250 let output = self
251 .bytes
252 .as_mut_slice()
253 .get_mut(self.used..next_used)
254 .ok_or(PaginationError::HistoryBudgetExceeded)?;
255 output
256 .get_mut(..2)
257 .ok_or(PaginationError::HistoryBudgetExceeded)?
258 .copy_from_slice(&state_len.to_be_bytes());
259 output
260 .get_mut(2..HISTORY_PREFIX_BYTES)
261 .ok_or(PaginationError::HistoryBudgetExceeded)?
262 .copy_from_slice(&digest.0);
263 output
264 .get_mut(HISTORY_PREFIX_BYTES..)
265 .ok_or(PaginationError::HistoryBudgetExceeded)?
266 .copy_from_slice(state);
267 self.used = next_used;
268 self.entries = self
269 .entries
270 .checked_add(1)
271 .ok_or(PaginationError::HistoryBudgetExceeded)?;
272 Ok(())
273 }
274}
275
276impl Drop for CursorHistory<'_> {
277 fn drop(&mut self) {
278 sanitize_value(&mut self.used);
279 sanitize_value(&mut self.entries);
280 }
281}
282
283impl fmt::Debug for CursorHistory<'_> {
284 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
285 formatter
286 .debug_struct("CursorHistory")
287 .field("entries", &self.entries)
288 .field("state", &"[redacted]")
289 .finish()
290 }
291}