use super::register::EntryHash;
use crate::safeurl::{ContentType, SafeUrl, XorUrl};
use crate::{Error, Result, Safe};
use sn_interface::types::DataAddress;
use log::debug;
use rand::Rng;
use std::collections::BTreeSet;
use xor_name::XorName;
pub type MultimapKey = Vec<u8>;
pub type MultimapValue = Vec<u8>;
pub type MultimapKeyValue = (MultimapKey, MultimapValue);
pub type Multimap = BTreeSet<(EntryHash, MultimapKeyValue)>;
const MULTIMAP_REMOVED_MARK: &[u8] = b"";
impl Safe {
pub async fn multimap_create(&self, name: Option<XorName>, type_tag: u64) -> Result<XorUrl> {
debug!("Creating a Multimap");
self.register_create(name, type_tag, ContentType::Multimap)
.await
}
pub async fn multimap_get_by_key(&self, url: &str, key: &[u8]) -> Result<Multimap> {
debug!("Getting value by key from Multimap at: {}", url);
let safeurl = self.parse_and_resolve_url(url).await?;
self.fetch_multimap_values_by_key(&safeurl, key).await
}
pub async fn multimap_get_by_hash(
&self,
url: &str,
hash: EntryHash,
) -> Result<MultimapKeyValue> {
debug!("Getting value by hash from Multimap at: {}", url);
let safeurl = self.parse_and_resolve_url(url).await?;
self.fetch_multimap_value_by_hash(&safeurl, hash).await
}
pub(crate) async fn fetch_multimap_values_by_key(
&self,
safeurl: &SafeUrl,
key: &[u8],
) -> Result<Multimap> {
let entries = self.fetch_multimap(safeurl).await?;
Ok(entries
.into_iter()
.filter(|(_, (entry_key, _))| entry_key == key)
.collect())
}
pub async fn multimap_insert(
&self,
multimap_url: &str,
entry: MultimapKeyValue,
replace: BTreeSet<EntryHash>,
) -> Result<EntryHash> {
debug!("Inserting '{:?}' into Multimap at {}", entry, multimap_url);
let serialised_entry = rmp_serde::to_vec_named(&entry).map_err(|err| {
Error::Serialisation(format!(
"Couldn't serialise the Multimap entry '{entry:?}': {err:?}",
))
})?;
let data = serialised_entry.clone();
let safeurl = SafeUrl::from_url(multimap_url)?;
let address = match safeurl.address() {
DataAddress::Register(reg_address) => reg_address,
other => {
return Err(Error::InvalidXorUrl(format!(
"The Multimap Url {multimap_url} has an {other:?} address.\
To insert an entry into a multimap, the address must be a register address.",
)))
}
};
if self.dry_run_mode {
return Ok(EntryHash(rand::thread_rng().gen::<[u8; 32]>()));
}
let client = self.get_safe_client()?;
let (entry_hash, op_batch) = client
.write_to_local_register(address, data, replace)
.await?;
client.publish_register_ops(op_batch).await?;
Ok(entry_hash)
}
pub async fn multimap_remove(
&self,
url: &str,
to_remove: BTreeSet<EntryHash>,
) -> Result<EntryHash> {
debug!("Removing from Multimap at {}: {:?}", url, to_remove);
let safeurl = SafeUrl::from_url(url)?;
let address = match safeurl.address() {
DataAddress::Register(reg_address) => reg_address,
other => {
return Err(Error::InvalidXorUrl(format!(
"The multimap url {url} has an {other:?} address.\
To remove an entry from a multimap, the address must be a register address.",
)))
}
};
if self.dry_run_mode {
return Ok(EntryHash(rand::thread_rng().gen::<[u8; 32]>()));
}
let client = self.get_safe_client()?;
let (entry_hash, op_batch) = client
.write_to_local_register(address, MULTIMAP_REMOVED_MARK.to_vec(), to_remove)
.await?;
client.publish_register_ops(op_batch).await?;
Ok(entry_hash)
}
pub(crate) async fn fetch_multimap(&self, safeurl: &SafeUrl) -> Result<Multimap> {
let entries = match self.register_fetch_entries(safeurl).await {
Ok(data) => {
debug!("Multimap retrieved with {} entries...", data.len());
Ok(data)
}
Err(Error::EmptyContent(_)) => Err(Error::EmptyContent(format!(
"Multimap found at \"{safeurl}\" was empty"
))),
Err(Error::ContentNotFound(_)) => Err(Error::ContentNotFound(format!(
"No Multimap found at \"{safeurl}\""
))),
Err(Error::AccessDenied(_)) => {
return Err(Error::AccessDenied(format!(
"Couldn't read Multimap found at \"{safeurl}\""
)))
}
other => other,
}?;
let mut multimap = Multimap::new();
for (hash, entry) in &entries {
if entry == MULTIMAP_REMOVED_MARK {
continue;
}
let key_val = Self::decode_multimap_entry(entry)?;
multimap.insert((*hash, key_val));
}
Ok(multimap)
}
pub(crate) async fn fetch_multimap_value_by_hash(
&self,
safeurl: &SafeUrl,
hash: EntryHash,
) -> Result<MultimapKeyValue> {
let entry = match self.register_fetch_entry(safeurl, hash).await {
Ok(data) => {
debug!("Multimap retrieved...");
Ok(data)
}
Err(Error::EmptyContent(_)) => Err(Error::EmptyContent(format!(
"Multimap found at \"{safeurl}\" was empty"
))),
Err(Error::ContentNotFound(_)) => Err(Error::ContentNotFound(
"No Multimap found at this address".to_string(),
)),
Err(other) => Err(other),
}?;
if entry == MULTIMAP_REMOVED_MARK {
Err(Error::EmptyContent(format!(
"Entry found at \"{safeurl}\" is a tombstone (deletion marker)",
)))
} else {
let key_val = Self::decode_multimap_entry(&entry)?;
Ok(key_val)
}
}
fn decode_multimap_entry(entry: &[u8]) -> Result<MultimapKeyValue> {
rmp_serde::from_slice(entry)
.map_err(|err| Error::ContentError(format!("Couldn't parse Multimap entry: {err:?}")))
}
}
#[cfg(test)]
mod tests {
use crate::app::test_helpers::new_safe_instance;
use anyhow::Result;
use std::collections::BTreeSet;
#[tokio::test]
async fn test_multimap_create() -> Result<()> {
let safe = new_safe_instance().await?;
let xorurl = safe.multimap_create(None, 25_000).await?;
let key = b"".to_vec();
let received_data = safe.multimap_get_by_key(&xorurl, &key).await?;
assert_eq!(received_data, Default::default());
Ok(())
}
#[tokio::test]
async fn test_multimap_insert() -> Result<()> {
let safe = new_safe_instance().await?;
let key = b"key".to_vec();
let val = b"value".to_vec();
let key_val = (key.clone(), val.clone());
let val2 = b"value2".to_vec();
let key_val2 = (key.clone(), val2.clone());
let xorurl = safe.multimap_create(None, 25_000).await?;
let _ = safe.multimap_get_by_key(&xorurl, &key).await?;
let hash = safe
.multimap_insert(&xorurl, key_val.clone(), BTreeSet::new())
.await?;
let received_data = safe.multimap_get_by_key(&xorurl, &key).await?;
assert_eq!(
received_data,
vec![(hash, key_val.clone())].into_iter().collect()
);
let hashes_to_replace = vec![hash].into_iter().collect();
let hash2 = safe
.multimap_insert(&xorurl, key_val2.clone(), hashes_to_replace)
.await?;
let received_data = safe.multimap_get_by_key(&xorurl, &key).await?;
assert_eq!(
received_data,
vec![(hash2, key_val2.clone())].into_iter().collect()
);
Ok(())
}
#[tokio::test]
async fn test_multimap_get_by_hash() -> Result<()> {
let safe = new_safe_instance().await?;
let key = b"key".to_vec();
let val = b"value".to_vec();
let key_val = (key.clone(), val.clone());
let key2 = b"key2".to_vec();
let val2 = b"value2".to_vec();
let key_val2 = (key2.clone(), val2.clone());
let xorurl = safe.multimap_create(None, 25_000).await?;
let _ = safe.multimap_get_by_key(&xorurl, &key).await?;
let hash = safe
.multimap_insert(&xorurl, key_val.clone(), BTreeSet::new())
.await?;
let hash2 = safe
.multimap_insert(&xorurl, key_val2.clone(), BTreeSet::new())
.await?;
let received_data = safe.multimap_get_by_hash(&xorurl, hash).await?;
assert_eq!(received_data, key_val.clone());
let received_data = safe.multimap_get_by_hash(&xorurl, hash2).await?;
assert_eq!(received_data, key_val2.clone());
Ok(())
}
}