Skip to main content

sift_query_core/cursor/
codec.rs

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            // `NaiveDateTime`'s own `Display`/`FromStr` pair is not symmetric,
226            // so the value is written and read with one explicit format.
227            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}