mod nrs_map;
pub use nrs_map::{DefaultRdf, NrsMap};
use crate::{
api::app::{
consts::{CONTENT_ADDED_SIGN, CONTENT_DELETED_SIGN},
safeurl::{SafeContentType, SafeUrl, XorUrl},
Safe,
},
Error, Result,
};
use log::{debug, info, warn};
use std::collections::BTreeMap;
pub(crate) const NRS_MAP_TYPE_TAG: u64 = 1_500;
const ERROR_MSG_NO_NRS_MAP_FOUND: &str = "No NRS Map found at this address";
pub type ProcessedEntries = BTreeMap<String, (String, String)>;
impl Safe {
pub fn parse_url(url: &str) -> Result<SafeUrl> {
SafeUrl::from_url(&sanitised_url(url))
}
pub(crate) async fn parse_and_resolve_url(
&mut self,
url: &str,
) -> Result<(SafeUrl, Option<SafeUrl>)> {
let safe_url = Safe::parse_url(url)?;
let orig_path = safe_url.path_decoded()?;
let mut resolution_chain = self
.retrieve_from_url(
&safe_url.to_string(),
false,
None,
false,
)
.await?;
let safe_data = resolution_chain
.pop()
.ok_or_else(|| Error::ContentNotFound(format!("Failed to resolve {}", url)))?;
let mut safe_url = SafeUrl::from_url(&safe_data.xorurl())?;
safe_url.set_path(&orig_path);
if resolution_chain.is_empty() {
Ok((safe_url, None))
} else {
let nrsmap_xorul_encoder = SafeUrl::from_url(&resolution_chain[0].resolved_from())?;
Ok((safe_url, Some(nrsmap_xorul_encoder)))
}
}
pub async fn nrs_map_container_add(
&mut self,
name: &str,
link: &str,
default: bool,
hard_link: bool,
dry_run: bool,
) -> Result<(u64, XorUrl, ProcessedEntries, NrsMap)> {
info!("Adding to NRS map...");
let (safe_url, _) = validate_nrs_name(name)?;
let xorurl = safe_url.to_string();
let (version, mut nrs_map) = self.nrs_map_container_get(&xorurl).await?;
debug!("NRS, Existing data: {:?}", nrs_map);
let link = nrs_map.update(name, link, default, hard_link)?;
let mut processed_entries = ProcessedEntries::new();
processed_entries.insert(name.to_string(), (CONTENT_ADDED_SIGN.to_string(), link));
debug!("The new NRS Map: {:?}", nrs_map);
if !dry_run {
let nrs_map_xorurl = self.store_nrs_map(&nrs_map).await?;
self.safe_client
.append_to_sequence(
nrs_map_xorurl.as_bytes(),
safe_url.xorname(),
safe_url.type_tag(),
false,
)
.await?;
}
Ok((version + 1, xorurl, processed_entries, nrs_map))
}
pub async fn nrs_map_container_create(
&mut self,
name: &str,
link: &str,
default: bool,
hard_link: bool,
dry_run: bool,
) -> Result<(XorUrl, ProcessedEntries, NrsMap)> {
info!("Creating an NRS map");
let (_, nrs_url) = validate_nrs_name(name)?;
if self.nrs_map_container_get(&nrs_url).await.is_ok() {
Err(Error::ContentError(
"NRS name already exists. Please use 'nrs add' command to add sub names to it"
.to_string(),
))
} else {
let mut nrs_map = NrsMap::default();
let link = nrs_map.update(&name, link, default, hard_link)?;
let mut processed_entries = ProcessedEntries::new();
processed_entries.insert(name.to_string(), (CONTENT_ADDED_SIGN.to_string(), link));
debug!("The new NRS Map: {:?}", nrs_map);
if dry_run {
Ok(("".to_string(), processed_entries, nrs_map))
} else {
let nrs_xorname = SafeUrl::from_nrsurl(&nrs_url)?.xorname();
debug!("XorName for \"{:?}\" is \"{:?}\"", &nrs_url, &nrs_xorname);
let nrs_map_xorurl = self.store_nrs_map(&nrs_map).await?;
let xorname = self
.safe_client
.store_sequence(
nrs_map_xorurl.as_bytes(),
Some(nrs_xorname),
NRS_MAP_TYPE_TAG,
None,
false,
)
.await?;
let xorurl = SafeUrl::encode_sequence_data(
xorname,
NRS_MAP_TYPE_TAG,
SafeContentType::NrsMapContainer,
self.xorurl_base,
false,
)?;
Ok((xorurl, processed_entries, nrs_map))
}
}
}
pub async fn nrs_map_container_remove(
&mut self,
name: &str,
dry_run: bool,
) -> Result<(u64, XorUrl, ProcessedEntries, NrsMap)> {
info!("Removing from NRS map...");
let (safe_url, _) = validate_nrs_name(name)?;
let xorurl = safe_url.to_string();
let (version, mut nrs_map) = self.nrs_map_container_get(&xorurl).await?;
debug!("NRS, Existing data: {:?}", nrs_map);
let removed_link = nrs_map.nrs_map_remove_subname(name)?;
let mut processed_entries = ProcessedEntries::new();
processed_entries.insert(
name.to_string(),
(CONTENT_DELETED_SIGN.to_string(), removed_link),
);
debug!("The new NRS Map: {:?}", nrs_map);
if !dry_run {
let nrs_map_xorurl = self.store_nrs_map(&nrs_map).await?;
self.safe_client
.append_to_sequence(
nrs_map_xorurl.as_bytes(),
safe_url.xorname(),
safe_url.type_tag(),
false,
)
.await?;
}
Ok((version + 1, xorurl, processed_entries, nrs_map))
}
pub async fn nrs_map_container_get(&mut self, url: &str) -> Result<(u64, NrsMap)> {
debug!("Getting latest resolvable map container from: {:?}", url);
let safe_url = Safe::parse_url(url)?;
let data = match safe_url.content_version() {
None => {
self.safe_client
.sequence_get_last_entry(safe_url.xorname(), NRS_MAP_TYPE_TAG, false)
.await
}
Some(content_version) => {
let serialised_nrs_map = self
.safe_client
.sequence_get_entry(
safe_url.xorname(),
NRS_MAP_TYPE_TAG,
content_version,
false,
)
.await
.map_err(|_| {
Error::VersionNotFound(format!(
"Version '{}' is invalid for NRS Map Container found at \"{}\"",
content_version, url,
))
})?;
Ok((content_version, serialised_nrs_map))
}
};
match data {
Ok((version, nrs_map_xorurl_bytes)) => {
let url = String::from_utf8(nrs_map_xorurl_bytes).map_err(|err| {
Error::ContentError(format!(
"Couldn't parse the NrsMap link stored in the NrsMapContainer: {:?}",
err
))
})?;
debug!("Deserialised NrsMap XOR-URL: {}", url);
let nrs_map_xorurl = SafeUrl::from_url(&url)?;
let serialised_nrs_map = self.fetch_public_blob(&nrs_map_xorurl, None).await?;
debug!("Nrs map v{} retrieved: {:?} ", version, &serialised_nrs_map);
let nrs_map =
serde_json::from_str(&String::from_utf8_lossy(&serialised_nrs_map.as_slice()))
.map_err(|err| {
Error::ContentError(format!(
"Couldn't deserialise the NrsMap stored in the NrsContainer: {:?}",
err
))
})?;
Ok((version, nrs_map))
}
Err(Error::EmptyContent(_)) => {
warn!("Nrs container found at {:?} was empty", &url);
Ok((0, NrsMap::default()))
}
Err(Error::ContentNotFound(_)) => Err(Error::ContentNotFound(
ERROR_MSG_NO_NRS_MAP_FOUND.to_string(),
)),
Err(Error::VersionNotFound(msg)) => Err(Error::VersionNotFound(msg)),
Err(err) => Err(Error::NetDataError(format!(
"Failed to get current version: {}",
err
))),
}
}
async fn store_nrs_map(&mut self, nrs_map: &NrsMap) -> Result<String> {
let serialised_nrs_map = serde_json::to_string(nrs_map).map_err(|err| {
Error::Serialisation(format!(
"Couldn't serialise the NrsMap generated: {:?}",
err
))
})?;
let nrs_map_xorurl = self
.files_store_public_blob(serialised_nrs_map.as_bytes(), None, false)
.await?;
Ok(nrs_map_xorurl)
}
}
fn validate_nrs_name(name: &str) -> Result<(SafeUrl, String)> {
if name.find('/').is_some() {
let msg = "The NRS name/subname cannot contain a slash".to_string();
return Err(Error::InvalidInput(msg));
}
let sanitised_url = sanitised_url(name);
let safe_url = Safe::parse_url(&sanitised_url)?;
if safe_url.content_version().is_some() {
return Err(Error::InvalidInput(format!(
"The NRS name/subname URL cannot contain a version: {}",
sanitised_url
)));
};
Ok((safe_url, sanitised_url))
}
fn sanitised_url(name: &str) -> String {
format!("safe://{}", name.replace("safe://", ""))
}
#[cfg(test)]
mod tests {
use super::nrs_map::DefaultRdf;
use super::*;
use crate::{
api::app::{
consts::PREDICATE_LINK,
test_helpers::{new_safe_instance, random_nrs_name},
},
retry_loop, retry_loop_for_pattern,
};
use anyhow::{anyhow, bail, Result};
#[tokio::test]
async fn test_nrs_map_container_create() -> Result<()> {
let site_name = random_nrs_name();
let mut safe = new_safe_instance().await?;
let nrs_xorname = Safe::parse_url(&site_name)?.xorname();
let (xor_url, _, nrs_map) = safe
.nrs_map_container_create(
&site_name,
"safe://linked-from-site_name?v=0",
true,
false,
false,
)
.await?;
assert_eq!(nrs_map.sub_names_map.len(), 0);
assert_eq!(
nrs_map.get_default_link()?,
"safe://linked-from-site_name?v=0"
);
if let DefaultRdf::OtherRdf(def_data) = &nrs_map.default {
let link = def_data
.get(PREDICATE_LINK)
.ok_or_else(|| anyhow!("Entry not found with key '{}'", PREDICATE_LINK))?;
assert_eq!(*link, "safe://linked-from-site_name?v=0".to_string());
assert_eq!(
nrs_map.get_default()?,
&DefaultRdf::OtherRdf(def_data.clone())
);
let decoder = SafeUrl::from_url(&xor_url)?;
assert_eq!(nrs_xorname, decoder.xorname());
Ok(())
} else {
Err(anyhow!("No default definition map found...".to_string(),))
}
}
#[tokio::test]
async fn test_nrs_map_container_add() -> Result<()> {
let site_name = random_nrs_name();
let mut safe = new_safe_instance().await?;
let (link, _, _) = safe
.files_container_create(None, None, true, true, false)
.await?;
let link_v0 = format!("{}?v=0", link);
let (xorurl, _, nrs_map) = safe
.nrs_map_container_create(&format!("b.{}", site_name), &link_v0, true, false, false)
.await?;
assert_eq!(nrs_map.sub_names_map.len(), 1);
assert_eq!(nrs_map.get_default_link()?, link_v0);
let _ = retry_loop!(safe.fetch(&xorurl, None));
let link_v1 = format!("{}?v=1", link);
let (version, _, _, updated_nrs_map) = safe
.nrs_map_container_add(&format!("a.b.{}", site_name), &link_v1, true, false, false)
.await?;
assert_eq!(version, 1);
assert_eq!(updated_nrs_map.sub_names_map.len(), 1);
assert_eq!(updated_nrs_map.get_default_link()?, link_v1);
Ok(())
}
#[tokio::test]
async fn test_nrs_map_container_add_or_remove_with_versioned_target() -> Result<()> {
let site_name = random_nrs_name();
let mut safe = new_safe_instance().await?;
let (link, _, _) = safe
.files_container_create(None, None, true, true, false)
.await?;
let link_v0 = format!("{}?v=0", link);
let (xorurl, _, _) = safe
.nrs_map_container_create(&format!("b.{}", site_name), &link_v0, true, false, false)
.await?;
let _ = retry_loop!(safe.fetch(&xorurl, None));
let versioned_sitename = format!("a.b.{}?v=6", site_name);
match safe
.nrs_map_container_add(
&versioned_sitename,
"safe://linked-from-a_b_site_name?v=0",
true,
false,
false,
)
.await
{
Ok(_) => {
return Err(anyhow!(
"NRS map add was unexpectedly successful".to_string(),
))
}
Err(Error::InvalidInput(msg)) => assert_eq!(
msg,
format!(
"The NRS name/subname URL cannot contain a version: safe://{}",
versioned_sitename
)
),
other => bail!("Error returned is not the expected one: {:?}", other),
};
match safe
.nrs_map_container_remove(&versioned_sitename, false)
.await
{
Ok(_) => Err(anyhow!(
"NRS map remove was unexpectedly successful".to_string(),
)),
Err(Error::InvalidInput(msg)) => {
assert_eq!(
msg,
format!(
"The NRS name/subname URL cannot contain a version: safe://{}",
versioned_sitename
)
);
Ok(())
}
other => Err(anyhow!(
"Error returned is not the expected one: {:?}",
other
)),
}
}
#[tokio::test]
async fn test_nrs_map_container_remove_one_of_two() -> Result<()> {
let site_name = random_nrs_name();
let mut safe = new_safe_instance().await?;
let (link, _, _) = safe
.files_container_create(None, None, true, true, false)
.await?;
let link_v0 = format!("{}?v=0", link);
let (xorurl, _, nrs_map) = safe
.nrs_map_container_create(&format!("a.b.{}", site_name), &link_v0, true, false, false)
.await?;
assert_eq!(nrs_map.sub_names_map.len(), 1);
let _ = retry_loop!(safe.fetch(&xorurl, None));
let link_v1 = format!("{}?v=1", link);
let _ = safe
.nrs_map_container_add(&format!("a2.b.{}", site_name), &link_v1, true, false, false)
.await?;
let _ = retry_loop_for_pattern!(safe.nrs_map_container_get(&xorurl), Ok((version, _)) if *version == 1)?;
let (version, _, _, updated_nrs_map) = safe
.nrs_map_container_remove(&format!("a.b.{}", site_name), false)
.await?;
assert_eq!(version, 2);
assert_eq!(updated_nrs_map.sub_names_map.len(), 1);
assert_eq!(updated_nrs_map.get_default_link()?, link_v1);
Ok(())
}
#[tokio::test]
async fn test_nrs_map_container_remove_default_soft_link() -> Result<()> {
let site_name = random_nrs_name();
let mut safe = new_safe_instance().await?;
let (link, _, _) = safe
.files_container_create(None, None, true, true, false)
.await?;
let link_v0 = format!("{}?v=0", link);
let (xorurl, _, nrs_map) = safe
.nrs_map_container_create(&format!("a.b.{}", site_name), &link_v0, true, false, false)
.await?;
assert_eq!(nrs_map.sub_names_map.len(), 1);
let _ = retry_loop!(safe.fetch(&xorurl, None));
let (version, _, _, updated_nrs_map) = safe
.nrs_map_container_remove(&format!("a.b.{}", site_name), false)
.await?;
assert_eq!(version, 1);
assert_eq!(updated_nrs_map.sub_names_map.len(), 0);
match updated_nrs_map.get_default_link() {
Ok(link) => Err(anyhow!("Unexpectedly retrieved a default link: {}", link)),
Err(Error::ContentError(msg)) => {
assert_eq!(
msg,
"Default found for resolvable map (set to sub names 'a.b') cannot be resolved."
.to_string()
);
Ok(())
}
Err(err) => Err(anyhow!("Error returned is not the expected one: {}", err)),
}
}
#[tokio::test]
async fn test_nrs_map_container_remove_default_hard_link() -> Result<()> {
let site_name = random_nrs_name();
let mut safe = new_safe_instance().await?;
let (link, _, _) = safe
.files_container_create(None, None, true, true, false)
.await?;
let link_v0 = format!("{}?v=0", link);
let (xorurl, _, nrs_map) = safe
.nrs_map_container_create(
&format!("a.b.{}", site_name),
&link_v0,
true,
true,
false,
)
.await?;
assert_eq!(nrs_map.sub_names_map.len(), 1);
let _ = retry_loop!(safe.fetch(&xorurl, None));
let (version, _, _, updated_nrs_map) = safe
.nrs_map_container_remove(&format!("a.b.{}", site_name), false)
.await?;
assert_eq!(version, 1);
assert_eq!(updated_nrs_map.sub_names_map.len(), 0);
assert_eq!(updated_nrs_map.get_default_link()?, link_v0);
Ok(())
}
#[tokio::test]
async fn test_nrs_no_scheme() -> Result<()> {
let site_name = random_nrs_name();
let url = Safe::parse_url(&site_name)?;
assert_eq!(url.public_name(), site_name);
Ok(())
}
#[tokio::test]
async fn test_nrs_validate_name() -> Result<()> {
let nrs_name = random_nrs_name();
let (_, nrs_url) = validate_nrs_name(&nrs_name)?;
assert_eq!(nrs_url, format!("safe://{}", nrs_name));
Ok(())
}
#[tokio::test]
async fn test_nrs_validate_name_with_slash() -> Result<()> {
let nrs_name = "name/with/slash";
match validate_nrs_name(&nrs_name) {
Ok(_) => Err(anyhow!(
"Unexpectedly validated nrs name with slashes {}",
nrs_name
)),
Err(Error::InvalidInput(msg)) => {
assert_eq!(
msg,
"The NRS name/subname cannot contain a slash".to_string()
);
Ok(())
}
Err(err) => Err(anyhow!("Error returned is not the expected one: {}", err)),
}
}
}