use super::{
consts::{CONTENT_ADDED_SIGN, CONTENT_DELETED_SIGN},
nrs_map::NrsMap,
xorurl::SafeContentType,
Safe, SafeApp,
};
use crate::{
xorurl::{XorUrl, XorUrlEncoder},
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<XorUrlEncoder> {
XorUrlEncoder::from_url(&sanitised_url(url))
}
pub(crate) async fn parse_and_resolve_url(
&self,
url: &str,
) -> Result<(XorUrlEncoder, Option<XorUrlEncoder>)> {
let xorurl_encoder = Safe::parse_url(url)?;
let orig_path = xorurl_encoder.path_decoded()?;
let mut resolution_chain = self
.retrieve_from_url(
&xorurl_encoder.to_string(),
false,
None,
false, )
.await?;
let safe_data = match resolution_chain.pop() {
Some(safe_data) => Ok(safe_data),
None => {
Err(Error::Unexpected(format!("Failed to resolve {}", url)))
}
}?;
let mut xorurl_encoder = XorUrlEncoder::from_url(&safe_data.xorurl())?;
xorurl_encoder.set_path(&orig_path);
if resolution_chain.is_empty() {
Ok((xorurl_encoder, None))
} else {
let nrsmap_xorul_encoder =
XorUrlEncoder::from_url(&resolution_chain[0].resolved_from())?;
Ok((xorurl_encoder, 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 (xorurl_encoder, _) = validate_nrs_name(name)?;
let xorurl = xorurl_encoder.to_string();
let (version, mut nrs_map) = self.nrs_map_container_get(&xorurl).await?;
debug!("NRS, Existing data: {:?}", nrs_map);
let link = nrs_map.nrs_map_update_or_create_data(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_raw_data = gen_nrs_map_raw_data(&nrs_map)?;
self.safe_app
.sequence_append(
&nrs_map_raw_data,
xorurl_encoder.xorname(),
xorurl_encoder.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.nrs_map_update_or_create_data(&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 = XorUrlEncoder::from_nrsurl(&nrs_url)?.xorname();
debug!("XorName for \"{:?}\" is \"{:?}\"", &nrs_url, &nrs_xorname);
let nrs_map_raw_data = gen_nrs_map_raw_data(&nrs_map)?;
let xorname = self
.safe_app
.store_sequence_data(
&nrs_map_raw_data,
Some(nrs_xorname),
NRS_MAP_TYPE_TAG,
None,
false,
)
.await?;
let xorurl = XorUrlEncoder::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 (xorurl_encoder, _) = validate_nrs_name(name)?;
let xorurl = xorurl_encoder.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_raw_data = gen_nrs_map_raw_data(&nrs_map)?;
self.safe_app
.sequence_append(
&nrs_map_raw_data,
xorurl_encoder.xorname(),
xorurl_encoder.type_tag(),
false,
)
.await?;
}
Ok((version + 1, xorurl, processed_entries, nrs_map))
}
pub async fn nrs_map_container_get(&self, url: &str) -> Result<(u64, NrsMap)> {
debug!("Getting latest resolvable map container from: {:?}", url);
let xorurl_encoder = Safe::parse_url(url)?;
let data = match xorurl_encoder.content_version() {
None => {
self.safe_app
.sequence_get_last_entry(xorurl_encoder.xorname(), NRS_MAP_TYPE_TAG, false)
.await
}
Some(content_version) => {
let serialised_nrs_map = self
.safe_app
.sequence_get_entry(
xorurl_encoder.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, serialised_nrs_map)) => {
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
))),
}
}
}
fn validate_nrs_name(name: &str) -> Result<(XorUrlEncoder, String)> {
let sanitised_url = sanitised_url(name);
let xorurl_encoder = Safe::parse_url(&sanitised_url)?;
if xorurl_encoder.content_version().is_some() {
return Err(Error::InvalidInput(format!(
"The NRS name/subname URL cannot cannot contain a version: {}",
sanitised_url
)));
};
Ok((xorurl_encoder, sanitised_url))
}
fn sanitised_url(name: &str) -> String {
format!("safe://{}", name.replace("safe://", ""))
}
fn gen_nrs_map_raw_data(nrs_map: &NrsMap) -> Result<Vec<u8>> {
let serialised_nrs_map = serde_json::to_string(nrs_map).map_err(|err| {
Error::Unexpected(format!(
"Couldn't serialise the NrsMap generated: {:?}",
err
))
})?;
Ok(serialised_nrs_map.as_bytes().to_vec())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::app::test_helpers::{new_safe_instance, random_nrs_name};
#[tokio::test]
async fn test_nrs_map_container_create() -> Result<()> {
use crate::api::app::consts::FAKE_RDF_PREDICATE_LINK;
use crate::nrs_map::DefaultRdf;
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, _entries, 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(FAKE_RDF_PREDICATE_LINK).ok_or_else(|| {
Error::Unexpected(format!(
"Entry not found with key '{}'",
FAKE_RDF_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 = XorUrlEncoder::from_url(&xor_url).unwrap();
assert_eq!(nrs_xorname, decoder.xorname());
Ok(())
} else {
Err(Error::Unexpected(
"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 (_xor_url, _entries, nrs_map) = safe
.nrs_map_container_create(
&format!("b.{}", site_name),
"safe://linked-from-<b.site_name>?v=0",
true,
false,
false,
)
.await?;
assert_eq!(nrs_map.sub_names_map.len(), 1);
assert_eq!(
nrs_map.get_default_link()?,
"safe://linked-from-<b.site_name>?v=0"
);
let (version, _xorurl, _entries, updated_nrs_map) = safe
.nrs_map_container_add(
&format!("a.b.{}", site_name),
"safe://linked-from-<a.b.site_name>?v=0",
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()?,
"safe://linked-from-<a.b.site_name>?v=0"
);
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 _ = safe
.nrs_map_container_create(
&format!("b.{}", site_name),
"safe://linked-from-<b.site_name>?v=0",
true,
false,
false,
)
.await?;
let versioned_sitename = format!("safe://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(Error::Unexpected(
"NRS map add was unexpectedly successful".to_string(),
))
}
Err(err) => assert_eq!(
err,
Error::InvalidInput(format!(
"The NRS name/subname URL cannot cannot contain a version: {}",
versioned_sitename
))
),
};
match safe
.nrs_map_container_remove(&versioned_sitename, false)
.await
{
Ok(_) => Err(Error::Unexpected(
"NRS map remove was unexpectedly successful".to_string(),
)),
Err(err) => {
assert_eq!(
err,
Error::InvalidInput(format!(
"The NRS name/subname URL cannot cannot contain a version: {}",
versioned_sitename
))
);
Ok(())
}
}
}
#[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 (_xor_url, _entries, nrs_map) = safe
.nrs_map_container_create(
&format!("a.b.{}", site_name),
"safe://linked-from-<a.b.site_name>?v=0",
true,
false,
false,
)
.await?;
assert_eq!(nrs_map.sub_names_map.len(), 1);
let (_version, _xorurl, _entries, _updated_nrs_map) = safe
.nrs_map_container_add(
&format!("a2.b.{}", site_name),
"safe://linked-from-<a2.b.site_name>?v=0",
true,
false,
false,
)
.await?;
let (version, _xorurl, _entries, 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()?,
"safe://linked-from-<a2.b.site_name>?v=0"
);
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 (_xor_url, _entries, nrs_map) = safe
.nrs_map_container_create(
&format!("a.b.{}", site_name),
"safe://linked-from-<a.b.site_name>?v=0",
true,
false,
false,
)
.await?;
assert_eq!(nrs_map.sub_names_map.len(), 1);
let (version, _xorurl, _entries, 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(Error::Unexpected(format!(
"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(Error::Unexpected(format!(
"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 (_xor_url, _entries, nrs_map) = safe
.nrs_map_container_create(
&format!("a.b.{}", site_name),
"safe://linked-from-<a.b.site_name>?v=0",
true,
true, false,
)
.await?;
assert_eq!(nrs_map.sub_names_map.len(), 1);
let (version, _xorurl, _entries, 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()?,
"safe://linked-from-<a.b.site_name>?v=0"
);
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(())
}
}