Skip to main content

icydb_core/db/
cursor.rs

1///
2/// Cursor codec helpers.
3///
4/// This module owns the opaque wire-token format used for continuation cursors.
5/// It intentionally contains only token encoding/decoding logic and no query semantics.
6///
7
8/// Encode raw cursor bytes as a lowercase hex token.
9#[must_use]
10pub fn encode_cursor(bytes: &[u8]) -> String {
11    let mut out = String::with_capacity(bytes.len() * 2);
12    for byte in bytes {
13        use std::fmt::Write as _;
14        let _ = write!(out, "{byte:02x}");
15    }
16    out
17}
18
19/// Decode a lowercase/uppercase hex cursor token into raw bytes.
20///
21/// The token may include surrounding whitespace, which is trimmed.
22/// Returns a descriptive error string for invalid tokens.
23pub fn decode_cursor(token: &str) -> Result<Vec<u8>, String> {
24    let token = token.trim();
25    if token.is_empty() {
26        return Err("cursor token is empty".to_string());
27    }
28    if !token.len().is_multiple_of(2) {
29        return Err("cursor token must have an even number of hex characters".to_string());
30    }
31
32    let mut out = Vec::with_capacity(token.len() / 2);
33    let bytes = token.as_bytes();
34    for idx in (0..bytes.len()).step_by(2) {
35        let hi = decode_hex_nibble(bytes[idx])
36            .ok_or_else(|| format!("invalid hex character at position {}", idx + 1))?;
37        let lo = decode_hex_nibble(bytes[idx + 1])
38            .ok_or_else(|| format!("invalid hex character at position {}", idx + 2))?;
39        out.push((hi << 4) | lo);
40    }
41
42    Ok(out)
43}
44
45const fn decode_hex_nibble(byte: u8) -> Option<u8> {
46    match byte {
47        b'0'..=b'9' => Some(byte - b'0'),
48        b'a'..=b'f' => Some(byte - b'a' + 10),
49        b'A'..=b'F' => Some(byte - b'A' + 10),
50        _ => None,
51    }
52}