use crate::cache::{self, DiskArtifacts, MemoryCache};
use crate::error::{ResolveError, Result};
use crate::ladder::{self, Endpoint, EndpointKind};
use crate::pages;
use crate::transport::HttpTransport;
use crate::urn::ParsedUrn;
use crate::{node, rpc};
#[cfg(any(feature = "native", feature = "wasm"))]
use crate::{content_type, crypto};
use std::cell::RefCell;
pub(crate) struct Fetched {
pub data: ResolvedData,
pub root: Option<String>,
#[cfg_attr(not(any(feature = "native", feature = "wasm")), allow(dead_code))]
pub artifacts: Option<DiskArtifacts>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedData {
pub bytes: Vec<u8>,
pub content_type: String,
}
impl ResolvedData {
pub fn new(bytes: Vec<u8>, content_type: String) -> Self {
ResolvedData {
bytes,
content_type,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolveOutcome {
Success(ResolvedData),
IntegrityFailure,
Unreachable,
}
impl ResolveOutcome {
pub fn is_success(&self) -> bool {
matches!(self, ResolveOutcome::Success(_))
}
pub fn data(&self) -> Option<&ResolvedData> {
match self {
ResolveOutcome::Success(d) => Some(d),
_ => None,
}
}
pub fn kind(&self) -> &'static str {
match self {
ResolveOutcome::Success(_) => "success",
ResolveOutcome::IntegrityFailure => "integrity_failure",
ResolveOutcome::Unreachable => "unreachable",
}
}
pub fn render(&self, connect_url: &str) -> ResolvedData {
match self {
ResolveOutcome::Success(d) => d.clone(),
ResolveOutcome::IntegrityFailure => ResolvedData::new(
pages::integrity_failure_html().into_bytes(),
pages::HTML_CONTENT_TYPE.to_string(),
),
ResolveOutcome::Unreachable => ResolvedData::new(
pages::unreachable_html(connect_url).into_bytes(),
pages::HTML_CONTENT_TYPE.to_string(),
),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ResolveOptions {
pub endpoint: Option<String>,
pub connect_url: Option<String>,
pub cache_path: Option<String>,
}
pub struct Resolver<T: HttpTransport + ?Sized> {
options: ResolveOptions,
plan_cache: RefCell<Option<Vec<Endpoint>>>,
memory: MemoryCache,
#[cfg(any(feature = "native", feature = "wasm"))]
disk: Option<cache::DiskCache>,
transport: T,
}
impl<T: HttpTransport> Resolver<T> {
pub fn new(transport: T) -> Self {
Resolver::with_options(transport, ResolveOptions::default())
}
pub fn with_options(transport: T, options: ResolveOptions) -> Self {
#[cfg(any(feature = "native", feature = "wasm"))]
let disk = options.cache_path.as_ref().map(cache::DiskCache::new);
Resolver {
plan_cache: RefCell::new(None),
memory: MemoryCache::new(cache::DEFAULT_MEMORY_ENTRIES, cache::DEFAULT_MEMORY_BYTES),
#[cfg(any(feature = "native", feature = "wasm"))]
disk,
options,
transport,
}
}
}
impl<T: HttpTransport + ?Sized> Resolver<T> {
pub fn connect_url(&self) -> &str {
self.options
.connect_url
.as_deref()
.unwrap_or(pages::DEFAULT_CONNECT_URL)
}
async fn plan(&self) -> Vec<Endpoint> {
if let Some(plan) = self.plan_cache.borrow().as_ref() {
return plan.clone();
}
let plan = ladder::build_plan(&self.transport, self.options.endpoint.as_deref()).await;
*self.plan_cache.borrow_mut() = Some(plan.clone());
plan
}
async fn fetch_from(&self, endpoint: &Endpoint, parsed: &ParsedUrn) -> Result<Fetched> {
match endpoint.kind {
EndpointKind::Node => node::fetch(&self.transport, &endpoint.base, parsed).await,
EndpointKind::Rpc => rpc::fetch(&self.transport, &endpoint.base, parsed).await,
}
}
fn cache_id(parsed: &ParsedUrn, root: &str) -> String {
cache::content_id(
&parsed.store_id_hex(),
root,
parsed.resource_key(),
parsed.salt.as_deref(),
)
}
#[cfg(any(feature = "native", feature = "wasm"))]
fn disk_get_verified(&self, parsed: &ParsedUrn, id: &str) -> Option<ResolveOutcome> {
let disk = self.disk.as_ref()?;
let root = parsed.root_hex()?; let art = disk.get(id)?;
match crypto::verify_and_decrypt(
parsed,
&art.ciphertext,
&art.proof_b64,
&root,
&art.chunk_lens,
) {
Ok(bytes) => {
let ct = content_type::derive(parsed.resource_key(), &bytes);
Some(ResolveOutcome::Success(ResolvedData::new(bytes, ct)))
}
Err(ResolveError::VerifyFailed(_)) | Err(ResolveError::DecryptFailed) => {
disk.remove(id);
Some(ResolveOutcome::IntegrityFailure)
}
Err(_) => {
disk.remove(id);
None
}
}
}
#[cfg(not(any(feature = "native", feature = "wasm")))]
fn disk_get_verified(&self, _parsed: &ParsedUrn, _id: &str) -> Option<ResolveOutcome> {
None
}
fn cache_success(&self, parsed: &ParsedUrn, fetched: &Fetched) {
let Some(root) = fetched.root.as_deref() else {
return; };
let id = Self::cache_id(parsed, root);
self.memory.put(id.clone(), fetched.data.clone());
#[cfg(any(feature = "native", feature = "wasm"))]
if let (Some(disk), Some(art)) = (self.disk.as_ref(), fetched.artifacts.as_ref()) {
disk.put(&id, art);
}
let _ = &id;
}
pub async fn resolve(&self, urn: &str) -> Result<ResolveOutcome> {
let parsed = ParsedUrn::parse(urn)?;
let pinned_id = parsed.root_hex().map(|root| Self::cache_id(&parsed, &root));
if let Some(id) = &pinned_id {
if let Some(data) = self.memory.get(id) {
return Ok(ResolveOutcome::Success(data)); }
if let Some(outcome) = self.disk_get_verified(&parsed, id) {
if let ResolveOutcome::Success(data) = &outcome {
self.memory.put(id.clone(), data.clone());
}
return Ok(outcome);
}
}
let plan = self.plan().await;
let last = plan.len().saturating_sub(1);
for (i, endpoint) in plan.iter().enumerate() {
match self.fetch_from(endpoint, &parsed).await {
Ok(fetched) => {
self.cache_success(&parsed, &fetched); return Ok(ResolveOutcome::Success(fetched.data));
}
Err(ResolveError::VerifyFailed(_)) | Err(ResolveError::DecryptFailed) => {
return Ok(ResolveOutcome::IntegrityFailure)
}
Err(ResolveError::Transport(_)) => {
if i == last {
return Ok(ResolveOutcome::Unreachable);
}
}
Err(other) => return Err(other),
}
}
Ok(ResolveOutcome::Unreachable)
}
pub async fn resolve_rendered(&self, urn: &str) -> Result<ResolvedData> {
Ok(self.resolve(urn).await?.render(self.connect_url()))
}
}