use vti_common::error::AppError;
use vti_common::store::{Batch, 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())
}
fn index_keys(cred: &StoredCredential) -> std::collections::BTreeSet<Vec<u8>> {
cred.index_terms()
.into_iter()
.map(|(field, value)| index_key(field, &value, &cred.id))
.collect()
}
pub(crate) fn stage_reindex(
batch: &mut Batch,
prev: Option<&StoredCredential>,
next: Option<&StoredCredential>,
) {
let old = prev.map(index_keys).unwrap_or_default();
let new = next.map(index_keys).unwrap_or_default();
for key in old.difference(&new) {
batch.remove(key.clone());
}
for key in new {
batch.insert_raw(key, SENTINEL.to_vec());
}
}
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"));
}
}