1use super::{Cursor, CursorValue, invalid_cursor};
2use crate::{ConfigError, OrderBy, ParamError, ScalarValue, SearchEntity};
3use base64::Engine;
4use hmac::{Hmac, Mac};
5use sha2::Sha256;
6use std::fmt;
7type HmacSha256 = Hmac<Sha256>;
8const VERSION: u8 = 1;
9const SIGNATURE_BYTES: usize = 32;
10
11#[derive(Clone)]
12pub struct CursorCodec {
13 key: Vec<u8>,
14}
15
16impl fmt::Debug for CursorCodec {
17 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
18 formatter
19 .debug_struct("CursorCodec")
20 .field("key", &"[REDACTED]")
21 .finish()
22 }
23}
24
25impl CursorCodec {
26 pub fn new(key: impl AsRef<[u8]>) -> Result<Self, ConfigError> {
27 let key = key.as_ref();
28 if key.len() < 32 {
29 return Err(ConfigError::Invalid(
30 "cursor signing key must contain at least 32 bytes".to_owned(),
31 ));
32 }
33 Ok(Self { key: key.to_vec() })
34 }
35
36 pub fn encode<E: SearchEntity>(
37 &self,
38 order: &[OrderBy],
39 values: &[CursorValue],
40 ) -> Result<String, ParamError> {
41 if order.len() != values.len() || order.len() > usize::from(u8::MAX) {
42 return Err(invalid_cursor("cursor value count does not match ordering"));
43 }
44 let mut payload = Vec::new();
45 payload.push(VERSION);
46 payload.extend_from_slice(&E::META.schema_fingerprint.to_be_bytes());
47 payload.push(u8::try_from(order.len()).map_err(|_| invalid_cursor("too many keys"))?);
48 for (item, value) in order.iter().zip(values) {
49 if E::cursor_key_non_null(item.field) && matches!(value, CursorValue::Null) {
50 return Err(invalid_cursor("non-null cursor key cannot contain NULL"));
51 }
52 payload.extend_from_slice(&item.field.0.to_be_bytes());
53 payload.push(match item.direction {
54 crate::SortOrder::Asc => 0,
55 crate::SortOrder::Desc => 1,
56 });
57 payload.push(match item.nulls {
58 crate::NullOrder::First => 0,
59 crate::NullOrder::Last => 1,
60 });
61 encode_value(value, &mut payload)?;
62 }
63 let mut mac = HmacSha256::new_from_slice(&self.key)
64 .map_err(|_| invalid_cursor("invalid signing key"))?;
65 mac.update(&payload);
66 payload.extend_from_slice(&mac.finalize().into_bytes());
67 Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(payload))
68 }
69
70 pub fn decode<E: SearchEntity>(
71 &self,
72 token: &str,
73 order: &[OrderBy],
74 ) -> Result<Cursor, ParamError> {
75 let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
76 .decode(token)
77 .map_err(|_| invalid_cursor("cursor is not valid base64url"))?;
78 if bytes.len() <= SIGNATURE_BYTES {
79 return Err(invalid_cursor("cursor is truncated"));
80 }
81 let split = bytes.len() - SIGNATURE_BYTES;
82 let (payload, signature) = bytes.split_at(split);
83 let mut mac = HmacSha256::new_from_slice(&self.key)
84 .map_err(|_| invalid_cursor("invalid signing key"))?;
85 mac.update(payload);
86 mac.verify_slice(signature)
87 .map_err(|_| invalid_cursor("cursor signature is invalid"))?;
88 let mut reader = Reader::new(payload);
89 if reader.u8()? != VERSION {
90 return Err(invalid_cursor("unsupported cursor version"));
91 }
92 if reader.u64()? != E::META.schema_fingerprint {
93 return Err(invalid_cursor("cursor belongs to another entity schema"));
94 }
95 let count = usize::from(reader.u8()?);
96 if count != order.len() {
97 return Err(invalid_cursor("cursor ordering length changed"));
98 }
99 let mut values = Vec::with_capacity(count);
100 for item in order {
101 if reader.u16()? != item.field.0
102 || reader.u8()?
103 != match item.direction {
104 crate::SortOrder::Asc => 0,
105 crate::SortOrder::Desc => 1,
106 }
107 || reader.u8()?
108 != match item.nulls {
109 crate::NullOrder::First => 0,
110 crate::NullOrder::Last => 1,
111 }
112 {
113 return Err(invalid_cursor("cursor ordering changed"));
114 }
115 let value = decode_value(&mut reader)?;
116 if E::cursor_key_non_null(item.field) && matches!(value, CursorValue::Null) {
117 return Err(invalid_cursor("non-null cursor key cannot contain NULL"));
118 }
119 values.push(value);
120 }
121 if !reader.is_empty() {
122 return Err(invalid_cursor("cursor contains trailing data"));
123 }
124 Ok(Cursor { values })
125 }
126}
127
128fn encode_value(value: &CursorValue, output: &mut Vec<u8>) -> Result<(), ParamError> {
129 match value {
130 CursorValue::Null => output.push(0),
131 CursorValue::Scalar(value) => match value {
132 ScalarValue::Bool(value) => output.extend_from_slice(&[1, u8::from(*value)]),
133 ScalarValue::I32(value) => {
134 output.push(2);
135 output.extend_from_slice(&value.to_be_bytes());
136 }
137 ScalarValue::I64(value) => {
138 output.push(3);
139 output.extend_from_slice(&value.to_be_bytes());
140 }
141 ScalarValue::F32(value) => {
142 output.push(4);
143 output.extend_from_slice(&value.to_bits().to_be_bytes());
144 }
145 ScalarValue::F64(value) => {
146 output.push(5);
147 output.extend_from_slice(&value.to_bits().to_be_bytes());
148 }
149 #[cfg(feature = "decimal")]
150 ScalarValue::Decimal(value) => encode_bytes(6, value.to_string().as_bytes(), output)?,
151 ScalarValue::Text(value) => encode_bytes(7, value.as_bytes(), output)?,
152 ScalarValue::Bytes(value) => encode_bytes(8, value, output)?,
153 #[cfg(feature = "chrono")]
154 ScalarValue::Date(value) => encode_bytes(9, value.to_string().as_bytes(), output)?,
155 #[cfg(feature = "chrono")]
156 ScalarValue::Time(value) => encode_bytes(10, value.to_string().as_bytes(), output)?,
157 #[cfg(feature = "chrono")]
158 ScalarValue::DateTime(value) => encode_bytes(
159 11,
160 value.format("%Y-%m-%dT%H:%M:%S%.f").to_string().as_bytes(),
161 output,
162 )?,
163 #[cfg(feature = "chrono")]
164 ScalarValue::Timestamp(value) => {
165 encode_bytes(12, value.to_rfc3339().as_bytes(), output)?;
166 }
167 #[cfg(feature = "uuid")]
168 ScalarValue::Uuid(value) => encode_bytes(13, value.as_bytes(), output)?,
169 #[cfg(feature = "json")]
170 ScalarValue::Json(_) => return Err(invalid_cursor("JSON cannot be a cursor key")),
171 #[cfg(feature = "postgres-types")]
172 ScalarValue::ArrayI64(_)
173 | ScalarValue::ArrayText(_)
174 | ScalarValue::Int8Range(_)
175 | ScalarValue::Network(_) => {
176 return Err(invalid_cursor("native types cannot be cursor keys"));
177 }
178 },
179 }
180 Ok(())
181}
182
183fn encode_bytes(tag: u8, value: &[u8], output: &mut Vec<u8>) -> Result<(), ParamError> {
184 let length = u32::try_from(value.len()).map_err(|_| invalid_cursor("cursor value too long"))?;
185 output.push(tag);
186 output.extend_from_slice(&length.to_be_bytes());
187 output.extend_from_slice(value);
188 Ok(())
189}
190
191fn decode_value(reader: &mut Reader<'_>) -> Result<CursorValue, ParamError> {
192 let tag = reader.u8()?;
193 let value = match tag {
194 0 => return Ok(CursorValue::Null),
195 1 => ScalarValue::Bool(reader.u8()? != 0),
196 2 => ScalarValue::I32(i32::from_be_bytes(reader.array()?)),
197 3 => ScalarValue::I64(i64::from_be_bytes(reader.array()?)),
198 4 => ScalarValue::F32(f32::from_bits(u32::from_be_bytes(reader.array()?))),
199 5 => ScalarValue::F64(f64::from_bits(u64::from_be_bytes(reader.array()?))),
200 #[cfg(feature = "decimal")]
201 6 => ScalarValue::Decimal(
202 reader
203 .string()?
204 .parse()
205 .map_err(|_| invalid_cursor("invalid decimal cursor value"))?,
206 ),
207 7 => ScalarValue::Text(reader.string()?.to_owned()),
208 8 => ScalarValue::Bytes(reader.bytes()?.to_vec()),
209 #[cfg(feature = "chrono")]
210 9 => ScalarValue::Date(
211 reader
212 .string()?
213 .parse()
214 .map_err(|_| invalid_cursor("invalid date cursor value"))?,
215 ),
216 #[cfg(feature = "chrono")]
217 10 => ScalarValue::Time(
218 reader
219 .string()?
220 .parse()
221 .map_err(|_| invalid_cursor("invalid time cursor value"))?,
222 ),
223 #[cfg(feature = "chrono")]
224 11 => ScalarValue::DateTime(
225 chrono::NaiveDateTime::parse_from_str(reader.string()?, "%Y-%m-%dT%H:%M:%S%.f")
228 .map_err(|_| invalid_cursor("invalid datetime cursor value"))?,
229 ),
230 #[cfg(feature = "chrono")]
231 12 => ScalarValue::Timestamp(
232 chrono::DateTime::parse_from_rfc3339(reader.string()?)
233 .map_err(|_| invalid_cursor("invalid timestamp cursor value"))?
234 .to_utc(),
235 ),
236 #[cfg(feature = "uuid")]
237 13 => ScalarValue::Uuid(
238 uuid::Uuid::from_slice(reader.bytes()?)
239 .map_err(|_| invalid_cursor("invalid UUID cursor value"))?,
240 ),
241 _ => return Err(invalid_cursor("unknown cursor value type")),
242 };
243 Ok(CursorValue::Scalar(value))
244}
245
246struct Reader<'input> {
247 input: &'input [u8],
248 position: usize,
249}
250
251impl<'input> Reader<'input> {
252 const fn new(input: &'input [u8]) -> Self {
253 Self { input, position: 0 }
254 }
255
256 fn is_empty(&self) -> bool {
257 self.position == self.input.len()
258 }
259
260 fn u8(&mut self) -> Result<u8, ParamError> {
261 let value = *self
262 .input
263 .get(self.position)
264 .ok_or_else(|| invalid_cursor("cursor is truncated"))?;
265 self.position += 1;
266 Ok(value)
267 }
268
269 fn u16(&mut self) -> Result<u16, ParamError> {
270 Ok(u16::from_be_bytes(self.array()?))
271 }
272
273 fn u64(&mut self) -> Result<u64, ParamError> {
274 Ok(u64::from_be_bytes(self.array()?))
275 }
276
277 fn array<const N: usize>(&mut self) -> Result<[u8; N], ParamError> {
278 let end = self
279 .position
280 .checked_add(N)
281 .ok_or_else(|| invalid_cursor("cursor is truncated"))?;
282 let slice = self
283 .input
284 .get(self.position..end)
285 .ok_or_else(|| invalid_cursor("cursor is truncated"))?;
286 self.position = end;
287 slice
288 .try_into()
289 .map_err(|_| invalid_cursor("cursor is truncated"))
290 }
291
292 fn bytes(&mut self) -> Result<&'input [u8], ParamError> {
293 let length = usize::try_from(u32::from_be_bytes(self.array()?))
294 .map_err(|_| invalid_cursor("cursor value is too long"))?;
295 let end = self
296 .position
297 .checked_add(length)
298 .ok_or_else(|| invalid_cursor("cursor is truncated"))?;
299 let value = self
300 .input
301 .get(self.position..end)
302 .ok_or_else(|| invalid_cursor("cursor is truncated"))?;
303 self.position = end;
304 Ok(value)
305 }
306
307 fn string(&mut self) -> Result<&'input str, ParamError> {
308 std::str::from_utf8(self.bytes()?).map_err(|_| invalid_cursor("cursor value is not UTF-8"))
309 }
310}