use crate::ids::ClusterId;
pub trait CursorSigner: Send + Sync {
fn tag(&self, msg: &[u8]) -> Vec<u8>;
}
#[must_use]
pub fn wrap(signer: &dyn CursorSigner, cluster: &ClusterId, cursor: &str) -> String {
let tag = signer.tag(&binding(cluster, cursor));
let mut out =
String::with_capacity(cluster.as_str().len() * 2 + tag.len() * 2 + 2 + cursor.len());
push_hex(&mut out, cluster.as_str().as_bytes());
out.push('.');
push_hex(&mut out, &tag);
out.push('.');
out.push_str(cursor);
out
}
#[must_use]
pub fn unwrap(signer: &dyn CursorSigner, token: &str) -> Option<(ClusterId, String)> {
let mut parts = token.splitn(3, '.');
let cluster_hex = parts.next()?;
let tag_hex = parts.next()?;
let cursor = parts.next()?;
let cluster = ClusterId::from(decode_hex_to_string(cluster_hex)?);
let expected = signer.tag(&binding(&cluster, cursor));
if hex_eq_ct(tag_hex, &expected) {
Some((cluster, cursor.to_owned()))
} else {
None
}
}
fn binding(cluster: &ClusterId, cursor: &str) -> Vec<u8> {
let mut msg = Vec::with_capacity(cluster.as_str().len() + 1 + cursor.len());
msg.extend_from_slice(cluster.as_str().as_bytes());
msg.push(0x1f);
msg.extend_from_slice(cursor.as_bytes());
msg
}
fn hex_eq_ct(hex: &str, expected: &[u8]) -> bool {
let hex = hex.as_bytes();
if hex.len() != expected.len() * 2 {
return false;
}
let mut diff = 0u8;
for (pair, &want) in hex.chunks_exact(2).zip(expected.iter()) {
match (hex_val(pair[0]), hex_val(pair[1])) {
(Some(hi), Some(lo)) => diff |= ((hi << 4) | lo) ^ want,
_ => diff |= 1,
}
}
diff == 0
}
fn push_hex(out: &mut String, bytes: &[u8]) {
const DIGITS: &[u8; 16] = b"0123456789abcdef";
for &b in bytes {
out.push(DIGITS[(b >> 4) as usize] as char);
out.push(DIGITS[(b & 0x0f) as usize] as char);
}
}
fn decode_hex_to_string(hex: &str) -> Option<String> {
if !hex.len().is_multiple_of(2) {
return None;
}
let bytes = hex.as_bytes();
let mut out = Vec::with_capacity(hex.len() / 2);
for pair in bytes.chunks_exact(2) {
let hi = hex_val(pair[0])?;
let lo = hex_val(pair[1])?;
out.push((hi << 4) | lo);
}
String::from_utf8(out).ok()
}
fn hex_val(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
#[cfg(test)]
#[path = "cursor_tests.rs"]
mod tests;