use super::{PrivateNodeHeader, TemporalKey};
use crate::{
error::FsError,
private::{
AccessKey, PrivateDirectory, PrivateFile, PrivateNodeContentSerializable, PrivateRef,
encrypted::Encrypted, forest::traits::PrivateForest, link::PrivateLink,
},
traits::Id,
};
use anyhow::{Result, bail};
use async_once_cell::OnceCell;
use async_recursion::async_recursion;
use chrono::{DateTime, Utc};
use rand_core::CryptoRngCore;
use skip_ratchet::{JumpSize, RatchetSeeker};
use std::{
cmp::Ordering,
collections::{BTreeMap, BTreeSet},
fmt::Debug,
};
use wnfs_common::{
BlockStore, Cid,
utils::{Arc, CondSend},
};
use wnfs_nameaccumulator::Name;
#[derive(Debug, Clone, PartialEq)]
pub enum PrivateNode {
File(Arc<PrivateFile>),
Dir(Arc<PrivateDirectory>),
}
impl PrivateNode {
pub fn upsert_mtime(&self, time: DateTime<Utc>) -> Self {
match self {
Self::File(file) => {
let mut file = (**file).clone();
file.content.metadata.upsert_mtime(time);
Self::File(Arc::new(file))
}
Self::Dir(dir) => {
let mut dir = (**dir).clone();
dir.content.metadata.upsert_mtime(time);
Self::Dir(Arc::new(dir))
}
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_recursion)]
#[cfg_attr(target_arch = "wasm32", async_recursion(?Send))]
pub(crate) async fn update_ancestry(
&mut self,
parent_name: &Name,
forest: &mut impl PrivateForest,
store: &impl BlockStore,
rng: &mut (impl CryptoRngCore + CondSend),
) -> Result<()> {
match self {
Self::File(file_rc) => {
let file = Arc::make_mut(file_rc);
file.prepare_key_rotation(parent_name, rng).await?;
}
Self::Dir(dir_rc) => {
let dir = Arc::make_mut(dir_rc);
for private_link in &mut dir.content.entries.values_mut() {
let mut node = private_link
.resolve_node(forest, store, Some(dir.header.name.clone()))
.await?
.clone();
node.update_ancestry(&dir.header.name, forest, store, rng)
.await?;
*private_link = PrivateLink::from(node);
}
dir.prepare_key_rotation(parent_name, rng);
}
}
Ok(())
}
#[inline]
pub fn get_header(&self) -> &PrivateNodeHeader {
match self {
Self::File(file) => &file.header,
Self::Dir(dir) => &dir.header,
}
}
#[allow(clippy::mutable_key_type)]
pub fn get_previous(&self) -> &BTreeSet<(usize, Encrypted<Cid>)> {
match self {
Self::File(file) => &file.content.previous,
Self::Dir(dir) => &dir.content.previous,
}
}
pub fn as_dir(&self) -> Result<Arc<PrivateDirectory>> {
Ok(match self {
Self::Dir(dir) => Arc::clone(dir),
_ => bail!(FsError::NotADirectory),
})
}
pub fn as_dir_mut(&mut self) -> Result<&mut Arc<PrivateDirectory>> {
Ok(match self {
Self::Dir(dir) => dir,
_ => bail!(FsError::NotADirectory),
})
}
pub fn as_file(&self) -> Result<Arc<PrivateFile>> {
Ok(match self {
Self::File(file) => Arc::clone(file),
_ => bail!(FsError::NotAFile),
})
}
pub fn is_dir(&self) -> bool {
matches!(self, Self::Dir(_))
}
pub fn is_file(&self) -> bool {
matches!(self, Self::File(_))
}
pub async fn search_latest(
&self,
forest: &impl PrivateForest,
store: &impl BlockStore,
) -> Result<PrivateNode> {
self.search_latest_nodes(forest, store)
.await?
.into_iter()
.next()
.ok_or(FsError::NotFound.into())
}
pub async fn reconcile_latest(
&mut self,
forest: &mut impl PrivateForest,
store: &impl BlockStore,
rng: &mut (impl CryptoRngCore + CondSend),
) -> Result<()> {
self.store(forest, store, rng).await?;
self.search_latest_reconciled(forest, store).await?;
Ok(())
}
pub async fn search_latest_reconciled(
&self,
forest: &impl PrivateForest,
store: &impl BlockStore,
) -> Result<PrivateNode> {
let mut header = self.get_header().clone();
let mut unmerged_heads = header.seek_unmerged_heads(forest, store).await?;
match unmerged_heads.pop_first() {
Some((cid, head)) => {
if unmerged_heads.is_empty() {
Ok(head)
} else {
Self::merge(header, (cid, head), unmerged_heads, forest, store).await
}
}
_ => {
Ok(self.clone())
}
}
}
pub(crate) async fn merge(
header: PrivateNodeHeader,
(cid, node): (Cid, PrivateNode),
nodes: BTreeMap<Cid, PrivateNode>,
forest: &impl PrivateForest,
store: &impl BlockStore,
) -> Result<PrivateNode> {
match node {
PrivateNode::File(mut file) => {
let files = nodes
.into_iter()
.filter_map(|(cid, node)| node.as_file().ok().map(|file| (cid, file)))
.collect::<BTreeMap<_, _>>();
for (other_cid, other_file) in files {
file.merge(header.clone(), cid, &other_file, other_cid)?;
}
Ok(PrivateNode::File(file))
}
PrivateNode::Dir(mut dir) => {
let dirs = nodes
.into_iter()
.filter_map(|(cid, node)| node.as_dir().ok().map(|dir| (cid, dir)))
.collect::<BTreeMap<_, _>>();
for (other_cid, other_dir) in dirs {
dir.merge(header.clone(), cid, &other_dir, other_cid, forest, store)
.await?;
}
Ok(PrivateNode::Dir(dir))
}
}
}
pub async fn search_latest_nodes(
&self,
forest: &impl PrivateForest,
store: &impl BlockStore,
) -> Result<Vec<PrivateNode>> {
let header = self.get_header();
let current_name = &header.get_revision_name();
if !forest.has(current_name, store).await? {
return Ok(vec![self.clone()]);
}
let mut search = RatchetSeeker::new(header.ratchet.clone(), JumpSize::Small);
let mut current_header = header.clone();
loop {
let current = search.current();
current_header.update_ratchet(current.clone());
let has_curr = forest
.has(¤t_header.get_revision_name(), store)
.await?;
let ord = if has_curr {
Ordering::Less
} else {
Ordering::Greater
};
if !search.step(ord) {
break;
}
}
current_header.update_ratchet(search.current().clone());
Ok(current_header
.get_multivalue(forest, store)
.await?
.into_iter()
.map(|(_, node)| node)
.collect())
}
pub(crate) async fn from_private_ref(
private_ref: &PrivateRef,
forest: &impl PrivateForest,
store: &impl BlockStore,
parent_name: Option<Name>,
) -> Result<PrivateNode> {
let cid = match forest
.get_encrypted_by_hash(&private_ref.label, store)
.await?
{
Some(cids) if cids.contains(&private_ref.content_cid) => private_ref.content_cid,
_ => bail!(FsError::NotFound),
};
Self::from_cid(cid, &private_ref.temporal_key, forest, store, parent_name).await
}
pub(crate) async fn from_cid(
cid: Cid,
temporal_key: &TemporalKey,
forest: &impl PrivateForest,
store: &impl BlockStore,
parent_name: Option<Name>,
) -> Result<PrivateNode> {
let encrypted_bytes = store.get_block(&cid).await?;
let snapshot_key = temporal_key.derive_snapshot_key();
let bytes = snapshot_key.decrypt(&encrypted_bytes)?;
let node: PrivateNodeContentSerializable = serde_ipld_dagcbor::from_slice(&bytes)?;
Ok(match node {
PrivateNodeContentSerializable::File(file) => {
let file = PrivateFile::from_serializable(
file,
temporal_key,
cid,
forest,
store,
parent_name,
)
.await?;
PrivateNode::File(Arc::new(file))
}
PrivateNodeContentSerializable::Dir(dir) => {
let dir = PrivateDirectory::from_serializable(
dir,
temporal_key,
cid,
forest,
store,
parent_name,
)
.await?;
PrivateNode::Dir(Arc::new(dir))
}
})
}
pub(crate) fn get_persisted_as(&self) -> &OnceCell<Cid> {
match self {
Self::Dir(dir) => &dir.content.persisted_as,
Self::File(file) => &file.content.persisted_as,
}
}
pub(crate) async fn store_and_get_private_ref(
&self,
forest: &mut impl PrivateForest,
store: &impl BlockStore,
rng: &mut (impl CryptoRngCore + CondSend),
) -> Result<PrivateRef> {
match self {
Self::File(file) => file.store(forest, store, rng).await,
Self::Dir(dir) => dir.store(forest, store, rng).await,
}
}
pub async fn load(
access_key: &AccessKey,
forest: &impl PrivateForest,
store: &impl BlockStore,
parent_name: Option<Name>,
) -> Result<PrivateNode> {
let private_ref = access_key.derive_private_ref()?;
PrivateNode::from_private_ref(&private_ref, forest, store, parent_name).await
}
pub async fn store(
&self,
forest: &mut impl PrivateForest,
store: &impl BlockStore,
rng: &mut (impl CryptoRngCore + CondSend),
) -> Result<AccessKey> {
let private_ref = &self.store_and_get_private_ref(forest, store, rng).await?;
Ok(AccessKey::Temporal(private_ref.into()))
}
}
impl Id for PrivateNode {
fn get_id(&self) -> String {
match self {
Self::File(file) => file.get_id(),
Self::Dir(dir) => dir.get_id(),
}
}
}
impl From<PrivateFile> for PrivateNode {
fn from(file: PrivateFile) -> Self {
Self::File(Arc::new(file))
}
}
impl From<PrivateDirectory> for PrivateNode {
fn from(dir: PrivateDirectory) -> Self {
Self::Dir(Arc::new(dir))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::private::forest::hamt::HamtForest;
use rand_chacha::ChaCha12Rng;
use rand_core::SeedableRng;
use wnfs_common::MemoryBlockStore;
#[async_std::test]
async fn serialized_private_node_can_be_deserialized() {
let rng = &mut ChaCha12Rng::seed_from_u64(0);
let content = b"Lorem ipsum dolor sit amet";
let forest = &mut HamtForest::new_rsa_2048_rc(rng);
let store = &MemoryBlockStore::new();
let file = PrivateFile::with_content(
&forest.empty_name(),
Utc::now(),
content.to_vec(),
forest,
store,
rng,
)
.await
.unwrap();
let mut directory = PrivateDirectory::new_rc(&forest.empty_name(), Utc::now(), rng);
directory
.mkdir(&["music".into()], true, Utc::now(), forest, store, rng)
.await
.unwrap();
let file_node = PrivateNode::File(Arc::new(file));
let dir_node = PrivateNode::Dir(Arc::clone(&directory));
let file_private_ref = file_node.store(forest, store, rng).await.unwrap();
let dir_private_ref = dir_node.store(forest, store, rng).await.unwrap();
let deserialized_file_node =
PrivateNode::load(&file_private_ref, forest, store, Some(forest.empty_name()))
.await
.unwrap();
let deserialized_dir_node =
PrivateNode::load(&dir_private_ref, forest, store, Some(forest.empty_name()))
.await
.unwrap();
assert_eq!(file_node, deserialized_file_node);
assert_eq!(dir_node, deserialized_dir_node);
}
}