1use anyhow::{Context, Result, anyhow, bail};
2use base64::{Engine as _, engine::general_purpose};
3
4const CURSOR_PREFIX: &str = "offset:";
5
6#[must_use]
7pub fn encode_cursor(offset: usize) -> String {
8 let payload = format!("{CURSOR_PREFIX}{offset}");
9 general_purpose::URL_SAFE_NO_PAD.encode(payload.as_bytes())
10}
11
12pub fn decode_cursor(cursor: &str) -> Result<usize> {
18 if cursor.trim().is_empty() {
19 return Ok(0);
20 }
21
22 if let Ok(value) = cursor.parse::<usize>() {
23 return Ok(value);
24 }
25
26 if let Some(rest) = cursor.strip_prefix(CURSOR_PREFIX) {
27 return rest
28 .parse::<usize>()
29 .map_err(|_| anyhow!("Invalid cursor payload"));
30 }
31
32 let decoded = general_purpose::URL_SAFE_NO_PAD
33 .decode(cursor)
34 .with_context(|| format!("Failed to decode cursor '{cursor}'"))?;
35 let decoded_str = String::from_utf8(decoded)
36 .with_context(|| format!("Cursor '{cursor}' is not valid UTF-8"))?;
37
38 let Some(rest) = decoded_str.strip_prefix(CURSOR_PREFIX) else {
39 bail!("Invalid cursor payload");
40 };
41
42 rest.parse::<usize>()
43 .map_err(|_| anyhow!("Invalid cursor payload"))
44}