use crate::api::{SecretEntry, SecretEntryMatch, SecretList, SecretListFilter, SecretVersion, SecretVersionRef};
use crate::block_store::{Change, ChangeLog, Operation};
use crate::memguard::weak::ZeroingHeapAllocator;
use crate::memguard::SecretWords;
use crate::secrets_store::{SecretStoreError, SecretStoreResult};
use crate::secrets_store_capnp::{index, secret_entry};
use capnp::{message, serialize};
use itertools::Itertools;
use std::collections::{BTreeSet, HashMap, HashSet};
struct EffectiveChanges {
new_heads: HashMap<String, Change>,
added_versions: HashMap<String, HashMap<String, SecretVersion>>,
deleted_blocks: HashSet<String>,
}
impl EffectiveChanges {
fn is_empty(&self) -> bool {
self.added_versions.is_empty() && self.deleted_blocks.is_empty()
}
}
#[derive(Clone)]
pub struct Index {
heads: HashMap<String, Change>,
pub(super) data: SecretWords,
}
impl Index {
pub fn from_secured_raw(raw: &[u8]) -> SecretStoreResult<Index> {
let data = SecretWords::from_secured(raw);
let heads = Self::read_heads(&data)?;
Ok(Index { heads, data })
}
pub fn find_versions(&self, secret_id: &str) -> SecretStoreResult<Vec<SecretVersionRef>> {
let mut data_borrow: &[u8] = &self.data.borrow();
let reader = serialize::read_message_from_flat_slice(&mut data_borrow, message::ReaderOptions::new())?;
let index = reader.get_root::<index::Reader>()?;
for index_entry in index.get_entries()? {
if index_entry.get_entry()?.get_id()? == secret_id {
return Ok(
index_entry
.get_version_refs()?
.iter()
.map(SecretVersionRef::from_reader)
.collect::<capnp::Result<Vec<SecretVersionRef>>>()?,
);
}
}
Err(SecretStoreError::NotFound)
}
pub fn filter_entries(&self, filter: &SecretListFilter) -> SecretStoreResult<SecretList> {
let mut data_borrow: &[u8] = &self.data.borrow();
let reader = serialize::read_message_from_flat_slice(&mut data_borrow, message::ReaderOptions::new())?;
let index = reader.get_root::<index::Reader>()?;
let mut entries = Vec::new();
let mut all_tags = BTreeSet::new();
for index_entry in index.get_entries()? {
let entry = index_entry.get_entry()?;
for maybe_tag in entry.get_tags()? {
let tag = maybe_tag?;
if !all_tags.contains(tag) {
all_tags.insert(tag.to_string());
}
}
if let Some(entry_match) = Self::match_entry(entry, filter)? {
entries.push(entry_match);
}
}
entries.sort();
Ok(SecretList {
all_tags: all_tags.into_iter().collect(),
entries,
})
}
pub fn process_change_logs<F>(&mut self, change_logs: &[ChangeLog], version_accessor: F) -> SecretStoreResult<bool>
where
F: Fn(&str) -> SecretStoreResult<Option<SecretVersion>>,
{
let effective_changes = self.collect_changes(change_logs, &version_accessor)?;
if effective_changes.is_empty() {
return Ok(false); }
let to_keep = self.collect_entries_to_keep(&effective_changes.deleted_blocks)?;
let additions = effective_changes
.added_versions
.keys()
.filter(|id| !to_keep.contains(*id))
.count();
let mut index_message = message::Builder::new(ZeroingHeapAllocator::default());
{
let mut index_borrow: &[u8] = &self.data.borrow();
let reader = serialize::read_message_from_flat_slice(&mut index_borrow, message::ReaderOptions::new())?;
let old_index = reader.get_root::<index::Reader>()?;
let mut new_index = index_message.init_root::<index::Builder>();
Self::update_heads(new_index.reborrow(), &effective_changes.new_heads);
let mut entry_pos = 0;
let mut new_entries = new_index.init_entries((to_keep.len() + additions) as u32);
for old_index_entry in old_index.get_entries()? {
let old_entry = old_index_entry.get_entry()?;
let secret_id = old_entry.get_id()?;
if !to_keep.contains(secret_id) {
continue;
}
new_entries.set_with_caveats(entry_pos, old_index_entry)?;
Self::update_entry(
old_index_entry
.get_version_refs()?
.iter()
.map(SecretVersionRef::from_reader)
.collect::<capnp::Result<Vec<SecretVersionRef>>>()?,
new_entries.reborrow().get(entry_pos),
effective_changes.added_versions.get(secret_id),
&effective_changes.deleted_blocks,
&version_accessor,
)?;
entry_pos += 1;
}
for (secret_id, added_version) in effective_changes.added_versions {
if to_keep.contains(&secret_id) {
continue;
}
Self::update_entry(
vec![],
new_entries.reborrow().get(entry_pos),
Some(&added_version),
&effective_changes.deleted_blocks,
&version_accessor,
)?;
entry_pos += 1;
}
}
self.data = SecretWords::from(serialize::write_message_to_words(&index_message));
self.heads = effective_changes.new_heads;
Ok(true)
}
fn read_heads(index_data: &SecretWords) -> SecretStoreResult<HashMap<String, Change>> {
let mut index_borrow: &[u8] = &index_data.borrow();
let reader = serialize::read_message_from_flat_slice(&mut index_borrow, message::ReaderOptions::new())?;
let index = reader.get_root::<index::Reader>()?;
let mut heads = HashMap::with_capacity(index.get_heads()?.len() as usize);
for head in index.get_heads()? {
let node_id = head.get_node_id()?.to_string();
let op = match head.get_operation()? {
index::HeadOperation::Add => Operation::Add,
index::HeadOperation::Delete => Operation::Delete,
};
let block = head.get_block_id()?.to_string();
heads.insert(node_id, Change { op, block });
}
Ok(heads)
}
fn update_heads(index: index::Builder, heads: &HashMap<String, Change>) {
let mut new_heads = index.init_heads(heads.len() as u32);
for (idx, (node_id, change)) in heads.iter().enumerate() {
let mut new_head = new_heads.reborrow().get(idx as u32);
new_head.set_node_id(node_id);
match change.op {
Operation::Add => new_head.set_operation(index::HeadOperation::Add),
Operation::Delete => new_head.set_operation(index::HeadOperation::Delete),
}
new_head.set_block_id(&change.block);
}
}
fn collect_entries_to_keep(&self, deleted_blocks: &HashSet<String>) -> SecretStoreResult<HashSet<String>> {
let mut data_borrow: &[u8] = &self.data.borrow();
let reader = serialize::read_message_from_flat_slice(&mut data_borrow, message::ReaderOptions::new())?;
let index = reader.get_root::<index::Reader>()?;
let mut to_keep = HashSet::new();
for index_entry in index.get_entries()? {
let entry = index_entry.get_entry()?;
let secret_id = entry.get_id()?;
let mut remainging_count = 0;
for version_ref in index_entry.get_version_refs()? {
let block_id = version_ref.get_block_id()?;
if !deleted_blocks.contains(block_id) {
remainging_count += 1
}
}
if remainging_count > 0 {
to_keep.insert(secret_id.to_string());
}
}
Ok(to_keep)
}
fn collect_changes<F>(
&mut self,
change_logs: &[ChangeLog],
version_accessor: F,
) -> SecretStoreResult<EffectiveChanges>
where
F: Fn(&str) -> SecretStoreResult<Option<SecretVersion>>,
{
let mut new_heads = HashMap::with_capacity(change_logs.len());
let mut added_versions = HashMap::<String, HashMap<String, SecretVersion>>::new();
let mut deleted_blocks = HashSet::new();
for change_log in change_logs {
let changes = change_log.changes_since(self.heads.get(&change_log.node));
for change in changes {
match change.op {
Operation::Add => {
if let Some(secret_version) = version_accessor(&change.block)? {
let secret_id = secret_version.secret_id.clone();
let mut by_blocks = added_versions.remove(&secret_id).unwrap_or_default();
by_blocks.insert(change.block.clone(), secret_version);
added_versions.insert(secret_id, by_blocks);
}
}
Operation::Delete => {
deleted_blocks.insert(change.block.clone());
}
}
}
if let Some(last) = change_log.changes.last() {
new_heads.insert(change_log.node.clone(), last.clone());
}
}
for deleted_block in &deleted_blocks {
for by_block in added_versions.values_mut() {
by_block.remove(deleted_block);
}
}
Ok(EffectiveChanges {
new_heads,
added_versions,
deleted_blocks,
})
}
fn update_entry<F>(
old_version_refs: Vec<SecretVersionRef>,
mut new_entry: index::entry::Builder,
maybe_added_versions: Option<&HashMap<String, SecretVersion>>,
deleted_blocks: &HashSet<String>,
version_accessor: F,
) -> SecretStoreResult<()>
where
F: Fn(&str) -> SecretStoreResult<Option<SecretVersion>>,
{
let current_block_id = old_version_refs.first().map(|v| v.block_id.clone());
let mut version_refs =
Vec::with_capacity(old_version_refs.len() + maybe_added_versions.map(HashMap::len).unwrap_or(0));
for version_ref in old_version_refs {
if !deleted_blocks.contains(&version_ref.block_id) {
version_refs.push(version_ref)
}
}
if let Some(added_versions) = maybe_added_versions {
for (block_id, added_version) in added_versions {
if !deleted_blocks.contains(block_id) {
version_refs.push(SecretVersionRef {
block_id: block_id.clone(),
timestamp: added_version.timestamp,
})
}
}
}
version_refs.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
assert!(!version_refs.is_empty());
let new_current_block_id = version_refs.first().unwrap().block_id.clone();
if current_block_id.is_none() || current_block_id.unwrap() != new_current_block_id {
match maybe_added_versions.and_then(|added| added.get(&new_current_block_id)) {
Some(added_version) => added_version.to_entry_builder(new_entry.reborrow().init_entry())?,
None => version_accessor(&new_current_block_id)?
.unwrap()
.to_entry_builder(new_entry.reborrow().init_entry())?,
};
}
let mut entry_version_refs = new_entry.init_version_refs(version_refs.len() as u32);
for (idx, version_ref) in version_refs.iter().enumerate() {
version_ref.to_builder(entry_version_refs.reborrow().get(idx as u32));
}
Ok(())
}
fn match_entry(
entry_reader: secret_entry::Reader,
filter: &SecretListFilter,
) -> SecretStoreResult<Option<SecretEntryMatch>> {
let entry = SecretEntry::from_reader(entry_reader)?;
if filter.deleted != entry.deleted {
return Ok(None);
}
let (name_score, name_highlights) = match &filter.name {
Some(name_filter) => match sublime_fuzzy::best_match(name_filter, &entry.name) {
Some(fuzzy_match) => (
fuzzy_match.score(),
fuzzy_match.matched_indices().cloned().collect::<Vec<usize>>(),
),
_ => return Ok(None),
},
None => (0, vec![]),
};
let url_highlights = vec![];
let tags_highlights = match &filter.tag {
Some(tag_filter) => {
let highlights: Vec<usize> = entry
.tags
.iter()
.positions(|tag| tag.as_str() == tag_filter.as_str())
.collect();
if highlights.is_empty() {
return Ok(None);
}
highlights
}
None => vec![],
};
if !filter.secret_type.iter().all(|filter| filter == &entry.secret_type) {
return Ok(None);
}
Ok(Some(SecretEntryMatch {
entry,
name_score,
name_highlights,
url_highlights,
tags_highlights,
}))
}
}
impl Default for Index {
fn default() -> Self {
let mut index_message = message::Builder::new(ZeroingHeapAllocator::default());
index_message.init_root::<index::Builder>();
let index_data = serialize::write_message_to_words(&index_message);
Index {
data: index_data.into(),
heads: HashMap::new(),
}
}
}