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
88impl fmt::Debug for PaginationCursor<'_> {
89 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90 formatter.write_str("PaginationCursor([redacted])")
91 }
92}
93
94pub struct PaginationMarker<'storage> {
104 state: OpaqueState<'storage>,
105}
106
107impl<'storage> PaginationMarker<'storage> {
108 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 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#[derive(Clone, Copy, Eq, PartialEq)]
131pub struct CursorDigest([u8; DIGEST_BYTES]);
132
133impl CursorDigest {
134 #[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
147pub struct CursorHistory<'storage> {
154 bytes: SecretBuffer<'storage>,
155 used: usize,
156 entries: u32,
157 max_entries: u32,
158}
159
160impl<'storage> CursorHistory<'storage> {
161 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 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 #[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}