use std::{borrow::Cow, collections::HashMap};
use crate::{
crypto::AeadPack,
decode, encode,
events::{EventLogFile, WriteEvent},
vault::{secret::SecretId, Vault, VaultCommit},
Error, Result,
};
#[derive(Default)]
pub struct EventReducer<'a> {
vault: Option<Cow<'a, [u8]>>,
vault_name: Option<Cow<'a, str>>,
vault_meta: Option<Cow<'a, Option<AeadPack>>>,
secrets: HashMap<SecretId, Cow<'a, VaultCommit>>,
}
impl<'a> EventReducer<'a> {
pub fn new() -> Self {
Default::default()
}
pub async fn split(
vault: Vault,
) -> Result<(Vault, Vec<WriteEvent<'static>>)> {
let mut events = Vec::with_capacity(vault.len() + 1);
let header = vault.header().clone();
let head: Vault = header.into();
let buffer = encode(&head).await?;
events.push(WriteEvent::CreateVault(Cow::Owned(buffer)));
for (id, entry) in vault {
let event = WriteEvent::CreateSecret(id, Cow::Owned(entry));
events.push(event);
}
events.sort();
Ok((head, events))
}
pub async fn reduce(
mut self,
event_log: &'a EventLogFile,
) -> Result<EventReducer<'a>> {
let mut it = event_log.iter().await?;
if let Some(log) = it.next_entry().await? {
let event = event_log.event_data(&log).await?;
if let WriteEvent::CreateVault(vault) = event {
self.vault = Some(vault.clone());
while let Some(log) = it.next_entry().await? {
let event = event_log.event_data(&log).await?;
match event {
WriteEvent::CreateVault(_) => {
return Err(Error::CreateEventOnlyFirst)
}
WriteEvent::SetVaultName(name) => {
self.vault_name = Some(name.clone());
}
WriteEvent::SetVaultMeta(meta) => {
self.vault_meta = Some(meta.clone());
}
WriteEvent::CreateSecret(id, entry) => {
self.secrets.insert(id, entry.clone());
}
WriteEvent::UpdateSecret(id, entry) => {
self.secrets.insert(id, entry.clone());
}
WriteEvent::DeleteSecret(id) => {
self.secrets.remove(&id);
}
_ => {}
}
}
} else {
return Err(Error::CreateEventMustBeFirst);
}
}
Ok(self)
}
pub async fn compact(self) -> Result<Vec<WriteEvent<'a>>> {
if let Some(vault) = self.vault {
let mut events = Vec::new();
let mut vault: Vault = decode(&vault).await?;
if let Some(name) = self.vault_name {
vault.set_name(name.into_owned());
}
if let Some(meta) = self.vault_meta {
vault.header_mut().set_meta(meta.into_owned());
}
let buffer = encode(&vault).await?;
events.push(WriteEvent::CreateVault(Cow::Owned(buffer)));
for (id, entry) in self.secrets {
let entry = entry.into_owned();
events.push(WriteEvent::CreateSecret(id, Cow::Owned(entry)));
}
Ok(events)
} else {
Ok(Vec::new())
}
}
pub async fn build(self) -> Result<Vault> {
if let Some(vault) = self.vault {
let mut vault: Vault = decode(&vault).await?;
if let Some(name) = self.vault_name {
vault.set_name(name.into_owned());
}
if let Some(meta) = self.vault_meta {
vault.header_mut().set_meta(meta.into_owned());
}
for (id, entry) in self.secrets {
let entry = entry.into_owned();
vault.insert_entry(id, entry);
}
Ok(vault)
} else {
Ok(Default::default())
}
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod test {
use super::*;
use crate::{
commit::CommitHash,
crypto::PrivateKey,
decode,
events::{EventLogFile, WriteEvent},
test_utils::*,
vault::{
secret::{Secret, SecretId, SecretMeta},
VaultAccess, VaultCommit, VaultEntry,
},
};
use anyhow::Result;
use secrecy::ExposeSecret;
use tempfile::NamedTempFile;
async fn mock_event_log_file() -> Result<(
NamedTempFile,
EventLogFile,
Vec<CommitHash>,
PrivateKey,
SecretId,
)> {
let (encryption_key, _, _) = mock_encryption_key()?;
let (_, mut vault, buffer) = mock_vault_file().await?;
let temp = NamedTempFile::new()?;
let mut event_log = EventLogFile::new(temp.path()).await?;
let mut commits = Vec::new();
let event = WriteEvent::CreateVault(Cow::Owned(buffer));
commits.push(event_log.append_event(event).await?);
let (secret_id, _, _, _, event) =
mock_vault_note(&mut vault, &encryption_key, "foo", "bar")
.await?;
commits.push(event_log.append_event(event).await?);
let (_, _, _, event) = mock_vault_note_update(
&mut vault,
&encryption_key,
&secret_id,
"bar",
"qux",
)
.await?;
if let Some(event) = event {
commits.push(event_log.append_event(event).await?);
}
let (del_id, _, _, _, event) =
mock_vault_note(&mut vault, &encryption_key, "qux", "baz")
.await?;
commits.push(event_log.append_event(event).await?);
let event = vault.delete(&del_id).await?;
if let Some(event) = event {
commits.push(event_log.append_event(event).await?);
}
Ok((temp, event_log, commits, encryption_key, secret_id))
}
#[tokio::test]
async fn event_log_reduce_build() -> Result<()> {
let (temp, event_log, _, encryption_key, secret_id) =
mock_event_log_file().await?;
assert_eq!(5, event_log.tree().len());
let vault = EventReducer::new()
.reduce(&event_log)
.await?
.build()
.await?;
assert_eq!(1, vault.len());
let entry = vault.get(&secret_id);
assert!(entry.is_some());
if let Some(VaultCommit(_, VaultEntry(meta_aead, secret_aead))) =
entry
{
let meta = vault.decrypt(&encryption_key, meta_aead).await?;
let secret = vault.decrypt(&encryption_key, secret_aead).await?;
let meta: SecretMeta = decode(&meta).await?;
let secret: Secret = decode(&secret).await?;
assert_eq!("bar", meta.label());
assert_eq!("qux", {
match &secret {
Secret::Note { text, .. } => text.expose_secret(),
_ => panic!("unexpected secret type"),
}
});
}
temp.close()?;
Ok(())
}
#[tokio::test]
async fn event_log_reduce_compact() -> Result<()> {
let (_temp, event_log, _, _encryption_key, _secret_id) =
mock_event_log_file().await?;
assert_eq!(5, event_log.tree().len());
let vault = EventReducer::new()
.reduce(&event_log)
.await?
.build()
.await?;
let events = EventReducer::new()
.reduce(&event_log)
.await?
.compact()
.await?;
assert_eq!(2, events.len());
let compact_temp = NamedTempFile::new()?;
let mut compact = EventLogFile::new(compact_temp.path()).await?;
for event in events {
compact.append_event(event).await?;
}
let compact_vault =
EventReducer::new().reduce(&compact).await?.build().await?;
assert_eq!(vault, compact_vault);
Ok(())
}
}