use vti_common::error::AppError;
use vti_common::store::KeyspaceHandle;
use super::model::{IndexField, StoredCredential};
pub(crate) const INDEX_PREFIX: &str = "idx:";
const SENTINEL: &[u8] = b"1";
const VALUE_ID_SEP: u8 = 0x00;
fn index_key(field: IndexField, value: &str, id: &str) -> Vec<u8> {
let mut k = format!("{INDEX_PREFIX}{}:{value}", field.token()).into_bytes();
k.push(VALUE_ID_SEP);
k.extend_from_slice(id.as_bytes());
k
}
fn scan_prefix(field: IndexField, value: &str) -> Vec<u8> {
let mut p = format!("{INDEX_PREFIX}{}:{value}", field.token()).into_bytes();
p.push(VALUE_ID_SEP);
p
}
fn id_from_index_key(key: &[u8]) -> Option<String> {
let sep = key.iter().rposition(|&b| b == VALUE_ID_SEP)?;
let id = &key[sep + 1..];
if id.is_empty() {
return None;
}
std::str::from_utf8(id).ok().map(|s| s.to_string())
}
pub(crate) async fn insert_for(
vault: &KeyspaceHandle,
cred: &StoredCredential,
) -> Result<(), AppError> {
for (field, value) in cred.index_terms() {
vault
.insert_raw(index_key(field, &value, &cred.id), SENTINEL.to_vec())
.await?;
}
Ok(())
}
pub(crate) async fn remove_for(
vault: &KeyspaceHandle,
cred: &StoredCredential,
) -> Result<(), AppError> {
for (field, value) in cred.index_terms() {
vault.remove(index_key(field, &value, &cred.id)).await?;
}
Ok(())
}
pub(crate) async fn scan(
vault: &KeyspaceHandle,
field: IndexField,
value: &str,
) -> Result<Vec<String>, AppError> {
let rows = vault.prefix_iter_raw(scan_prefix(field, value)).await?;
let mut ids = Vec::with_capacity(rows.len());
for (key, _sentinel) in rows {
if let Some(id) = id_from_index_key(&key) {
ids.push(id);
}
}
Ok(ids)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn index_key_layout_is_field_value_nul_id() {
let k = index_key(IndexField::IssuerDid, "did:web:acme", "cred-1");
let mut expected = b"idx:issuer:did:web:acme".to_vec();
expected.push(0x00);
expected.extend_from_slice(b"cred-1");
assert_eq!(k, expected);
}
#[test]
fn scan_prefix_pins_value_with_nul() {
let p = scan_prefix(IndexField::CommunityDid, "did:web:acme");
let mut expected = b"idx:community:did:web:acme".to_vec();
expected.push(0x00);
assert_eq!(p, expected);
}
#[test]
fn colon_extended_value_is_not_a_false_prefix_match() {
let scan = scan_prefix(IndexField::CommunityDid, "did:web:acme");
let sibling = index_key(IndexField::CommunityDid, "did:web:acme:team", "x");
assert!(!sibling.starts_with(&scan));
}
#[test]
fn id_recovered_after_nul_even_when_value_has_colons() {
let k = index_key(IndexField::CommunityDid, "did:web:acme:team", "ulid-xyz");
assert_eq!(id_from_index_key(&k).as_deref(), Some("ulid-xyz"));
}
}