#![allow(clippy::disallowed_methods)]
#![allow(clippy::disallowed_types)]
use std::fs;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use super::pinned::PinnedIdentity;
use crate::error::TrustError;
const MAGIC: &[u8; 8] = b"AUTHSPI1";
const RECORD_LEN: usize = 32 + 8 + 8;
const MIN_STORE_BYTES_FOR_INDEX: u64 = 64 * 1024;
pub(crate) struct PinIndex {
path: PathBuf,
}
pub(crate) enum IndexLookup {
NotApplicable,
Resolved(Option<PinnedIdentity>),
}
struct Header {
store_len: u64,
store_mtime_nanos: u128,
entry_count: u64,
}
const HEADER_LEN: u64 = 8 + 8 + 16 + 8 ;
impl PinIndex {
pub(crate) fn for_store(store_path: &Path) -> Self {
Self {
path: store_path.with_extension("idx"),
}
}
pub(crate) fn lookup(&self, store_path: &Path, did: &str) -> Result<IndexLookup, TrustError> {
let fp = store_fingerprint(store_path)?;
if fp.0 < MIN_STORE_BYTES_FOR_INDEX {
return Ok(IndexLookup::NotApplicable);
}
let header = self.read_header().ok().flatten();
let fresh = matches!(
&header,
Some(h) if h.store_len == fp.0 && h.store_mtime_nanos == fp.1
);
if !fresh {
self.rebuild(store_path, fp)?;
}
let entry_count = match self.read_header()? {
Some(h) => h.entry_count,
None => return Ok(IndexLookup::Resolved(None)),
};
let Some((offset, len)) = self.find_span(did, entry_count)? else {
return Ok(IndexLookup::Resolved(None));
};
match read_entry_at(store_path, offset, len) {
Ok(Some(pin)) if pin.did == did => Ok(IndexLookup::Resolved(Some(pin))),
Ok(_) => Ok(IndexLookup::Resolved(None)),
Err(e) => Err(e),
}
}
fn find_span(&self, did: &str, entry_count: u64) -> Result<Option<(u64, u64)>, TrustError> {
if entry_count == 0 {
return Ok(None);
}
let key = did_hash(did);
let mut file = fs::File::open(&self.path)?;
let (mut lo, mut hi) = (0u64, entry_count);
while lo < hi {
let mid = lo + (hi - lo) / 2;
file.seek(SeekFrom::Start(HEADER_LEN + mid * RECORD_LEN as u64))?;
let mut rec_key = [0u8; 32];
file.read_exact(&mut rec_key)?;
match rec_key.cmp(&key) {
std::cmp::Ordering::Less => lo = mid + 1,
std::cmp::Ordering::Greater => hi = mid,
std::cmp::Ordering::Equal => {
let offset = file.read_u64::<LittleEndian>()?;
let len = file.read_u64::<LittleEndian>()?;
return Ok(Some((offset, len)));
}
}
}
Ok(None)
}
fn read_header(&self) -> Result<Option<Header>, TrustError> {
if !self.path.exists() {
return Ok(None);
}
let mut file = fs::File::open(&self.path)?;
let mut magic = [0u8; 8];
if file.read_exact(&mut magic).is_err() || &magic != MAGIC {
return Ok(None);
}
let store_len = file.read_u64::<LittleEndian>()?;
let mut mtime = [0u8; 16];
file.read_exact(&mut mtime)?;
let store_mtime_nanos = u128::from_le_bytes(mtime);
let entry_count = file.read_u64::<LittleEndian>()?;
Ok(Some(Header {
store_len,
store_mtime_nanos,
entry_count,
}))
}
fn rebuild(&self, store_path: &Path, fp: (u64, u128)) -> Result<(), TrustError> {
let content = fs::read(store_path).unwrap_or_default();
let mut records = scan_spans(&content)?;
records.sort_unstable_by(|a, b| a.0.cmp(&b.0));
let tmp = self.path.with_extension("idx.tmp");
{
let mut file = fs::File::create(&tmp)?;
file.write_all(MAGIC)?;
file.write_u64::<LittleEndian>(fp.0)?;
file.write_all(&fp.1.to_le_bytes())?;
file.write_u64::<LittleEndian>(records.len() as u64)?;
for (key, offset, len) in &records {
file.write_all(key)?;
file.write_u64::<LittleEndian>(*offset)?;
file.write_u64::<LittleEndian>(*len)?;
}
file.sync_all()?;
}
fs::rename(&tmp, &self.path)?;
Ok(())
}
pub(crate) fn invalidate(&self) {
let _ = fs::remove_file(&self.path);
}
}
fn did_hash(did: &str) -> [u8; 32] {
*blake3::hash(did.as_bytes()).as_bytes()
}
fn store_fingerprint(store_path: &Path) -> Result<(u64, u128), TrustError> {
let meta = fs::metadata(store_path)?;
let mtime_nanos = meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_nanos())
.unwrap_or(0);
Ok((meta.len(), mtime_nanos))
}
fn read_entry_at(
store_path: &Path,
offset: u64,
len: u64,
) -> Result<Option<PinnedIdentity>, TrustError> {
if len == 0 || len > (1 << 20) {
return Ok(None);
}
let mut file = fs::File::open(store_path)?;
let file_len = file.metadata()?.len();
if offset.saturating_add(len) > file_len {
return Ok(None);
}
file.seek(SeekFrom::Start(offset))?;
let mut buf = vec![0u8; len as usize];
if file.read_exact(&mut buf).is_err() {
return Ok(None);
}
Ok(serde_json::from_slice::<PinnedIdentity>(&buf).ok())
}
fn scan_spans(content: &[u8]) -> Result<Vec<([u8; 32], u64, u64)>, TrustError> {
let mut out = Vec::new();
let mut i = 0usize;
let n = content.len();
while i < n && content[i] != b'[' {
i += 1;
}
if i >= n {
return Ok(out); }
i += 1;
while i < n {
match content[i] {
b' ' | b'\t' | b'\r' | b'\n' | b',' => {
i += 1;
continue;
}
b']' => break,
b'{' => {}
_ => {
break;
}
}
let start = i;
let mut depth = 0i32;
let mut in_str = false;
let mut escaped = false;
while i < n {
let c = content[i];
if in_str {
if escaped {
escaped = false;
} else if c == b'\\' {
escaped = true;
} else if c == b'"' {
in_str = false;
}
} else {
match c {
b'"' => in_str = true,
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
i += 1;
break;
}
}
_ => {}
}
}
i += 1;
}
let end = i; let slice = &content[start..end];
if let Ok(pin) = serde_json::from_slice::<PinnedIdentity>(slice) {
out.push((did_hash(&pin.did), start as u64, (end - start) as u64));
}
}
Ok(out)
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use auths_verifier::PublicKeyHex;
fn store_with(dids: &[&str]) -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("known_identities.json");
let mut arr = String::from("[");
for (i, did) in dids.iter().enumerate() {
if i > 0 {
arr.push(',');
}
arr.push_str(&format!(
r#"{{"did":"{did}","public_key_hex":"{:064x}","curve":"ed25519","first_seen":"2024-01-01T00:00:00Z","origin":"manual","trust_level":"manual"}}"#,
i + 1
));
}
arr.push(']');
fs::write(&path, arr).unwrap();
(dir, path)
}
fn resolved(r: IndexLookup) -> Option<PinnedIdentity> {
match r {
IndexLookup::Resolved(p) => p,
IndexLookup::NotApplicable => {
panic!("expected the index path to apply for a large store")
}
}
}
fn big_dids(n: usize) -> Vec<String> {
(0..n).map(|i| format!("did:keri:E{i:043}")).collect()
}
#[test]
fn finds_present_entry_in_large_store() {
let dids = big_dids(4000);
let refs: Vec<&str> = dids.iter().map(|s| s.as_str()).collect();
let (_dir, path) = store_with(&refs);
let idx = PinIndex::for_store(&path);
let target = &dids[3999];
let pin = resolved(idx.lookup(&path, target).unwrap());
assert!(pin.is_some(), "indexed lookup must find a present DID");
assert_eq!(pin.unwrap().did, *target);
assert!(
path.with_extension("idx").exists(),
"sidecar must be written"
);
}
#[test]
fn absent_did_fails_closed() {
let dids = big_dids(4000);
let refs: Vec<&str> = dids.iter().map(|s| s.as_str()).collect();
let (_dir, path) = store_with(&refs);
let idx = PinIndex::for_store(&path);
let pin = resolved(idx.lookup(&path, "did:keri:Enot_in_the_store").unwrap());
assert!(pin.is_none(), "an unpinned DID must never resolve as found");
}
#[test]
fn rebuilds_when_store_changes() {
let mut dids = big_dids(4000);
let refs: Vec<&str> = dids.iter().map(|s| s.as_str()).collect();
let (dir, path) = store_with(&refs);
let idx = PinIndex::for_store(&path);
let present = dids[3999].clone();
let absent = "did:keri:Ewill_be_added_later";
assert!(resolved(idx.lookup(&path, &present).unwrap()).is_some());
assert!(resolved(idx.lookup(&path, absent).unwrap()).is_none());
dids.push(absent.to_string());
let refs2: Vec<&str> = dids.iter().map(|s| s.as_str()).collect();
std::thread::sleep(std::time::Duration::from_millis(5));
let path2 = dir.path().join("known_identities.json");
{
let mut arr = String::from("[");
for (i, did) in refs2.iter().enumerate() {
if i > 0 {
arr.push(',');
}
arr.push_str(&format!(
r#"{{"did":"{did}","public_key_hex":"{:064x}","curve":"ed25519","first_seen":"2024-01-01T00:00:00Z","origin":"manual","trust_level":"manual"}}"#,
i + 1
));
}
arr.push(']');
fs::write(&path2, arr).unwrap();
}
assert!(resolved(idx.lookup(&path, absent).unwrap()).is_some());
assert!(resolved(idx.lookup(&path, &present).unwrap()).is_some());
}
#[test]
fn small_store_is_not_indexed() {
let (_dir, path) = store_with(&["did:keri:Eaaa", "did:keri:Ebbb"]);
let idx = PinIndex::for_store(&path);
assert!(
matches!(
idx.lookup(&path, "did:keri:Eaaa").unwrap(),
IndexLookup::NotApplicable
),
"a tiny store must defer to the authoritative scan, no sidecar"
);
assert!(
!path.with_extension("idx").exists(),
"no sidecar for a small store"
);
}
#[test]
fn pretty_printed_store_is_indexable() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("known_identities.json");
let pins: Vec<PinnedIdentity> = (0..600)
.map(|i| PinnedIdentity {
did: format!("did:keri:E{i:043}"),
public_key_hex: PublicKeyHex::new_unchecked(format!("{:064x}", i + 1)),
curve: auths_crypto::CurveType::Ed25519,
kel_tip_said: None,
kel_sequence: None,
first_seen: chrono::Utc::now(),
origin: "manual".to_string(),
trust_level: super::super::pinned::TrustLevel::Manual,
})
.collect();
fs::write(&path, serde_json::to_string_pretty(&pins).unwrap()).unwrap();
let idx = PinIndex::for_store(&path);
let pin = resolved(
idx.lookup(
&path,
"did:keri:E0000000000000000000000000000000000000000599",
)
.unwrap(),
);
assert!(pin.is_some(), "pretty-printed store must index");
}
#[test]
fn confirm_read_rejects_out_of_range_or_garbage_span() {
let (_dir, path) = store_with(&["did:keri:Eaaa", "did:keri:Ebbb", "did:keri:Eccc"]);
assert!(
read_entry_at(&path, 0, 1).unwrap().is_none(),
"a 1-byte span never parses to a pin"
);
assert!(
read_entry_at(&path, u64::MAX - 1, 64).unwrap().is_none(),
"a span past EOF never parses to a pin"
);
}
}