#[cfg(test)]
mod tests;
use core::fmt;
use core::marker::PhantomData;
#[cfg(feature = "alloc")]
use alloc::collections::TryReserveError;
#[cfg(feature = "alloc")]
use alloc::string::String;
use crate::buffer::{Buffer, BufferTooSmallError, ByteSliceBuf};
use crate::components::RiReferenceComponents;
use crate::normalize::RemoveDotSegPath;
use crate::spec::Spec;
#[cfg(feature = "alloc")]
use crate::types::RiString;
use crate::types::{RiAbsoluteStr, RiReferenceStr, RiStr};
#[derive(Debug, Clone)]
pub struct Error {
repr: ErrorRepr,
}
impl Error {
#[must_use]
pub fn kind(&self) -> ErrorKind {
match &self.repr {
#[cfg(feature = "alloc")]
ErrorRepr::Alloc(_) => ErrorKind::OutOfMemory,
ErrorRepr::BufferFull(_) => ErrorKind::OutOfMemory,
ErrorRepr::Unresolvable => ErrorKind::Unresolvable,
}
}
}
impl fmt::Display for Error {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.repr {
#[cfg(feature = "alloc")]
ErrorRepr::Alloc(_) => f.write_str("IRI resolution failed: allocation failed"),
ErrorRepr::BufferFull(_) => f.write_str("IRI resolution failed: buffer full"),
ErrorRepr::Unresolvable => {
f.write_str("IRI resolution failed: unresolvable base and IRI pair")
}
}
}
}
impl From<ErrorRepr> for Error {
#[inline]
fn from(repr: ErrorRepr) -> Self {
Self { repr }
}
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.repr {
#[cfg(feature = "alloc")]
ErrorRepr::Alloc(e) => Some(e),
ErrorRepr::BufferFull(e) => Some(e),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub(crate) enum ErrorRepr {
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
Alloc(TryReserveError),
BufferFull(BufferTooSmallError),
Unresolvable,
}
impl From<BufferTooSmallError> for ErrorRepr {
#[inline]
fn from(e: BufferTooSmallError) -> Self {
Self::BufferFull(e)
}
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
impl From<TryReserveError> for ErrorRepr {
#[inline]
fn from(e: TryReserveError) -> Self {
Self::Alloc(e)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorKind {
Unresolvable,
OutOfMemory,
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn resolve<S: Spec>(
reference: impl AsRef<RiReferenceStr<S>>,
base: impl AsRef<RiAbsoluteStr<S>>,
) -> Result<RiString<S>, Error> {
FixedBaseResolver::new(base.as_ref()).resolve(reference.as_ref())
}
#[derive(Debug, Clone, Copy)]
pub struct FixedBaseResolver<'a, S: Spec> {
base_components: RiReferenceComponents<'a, S>,
}
impl<'a, S: Spec> FixedBaseResolver<'a, S> {
#[must_use]
pub fn new(base: &'a RiAbsoluteStr<S>) -> Self {
Self {
base_components: RiReferenceComponents::from(base.as_ref()),
}
}
}
impl<'a, S: Spec> FixedBaseResolver<'a, S> {
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn resolve(&self, reference: &RiReferenceStr<S>) -> Result<RiString<S>, Error> {
let mut buf = String::new();
self.create_task(reference).write_to_buf(&mut buf)?;
Ok(RiString::try_from(buf).expect("the resolved IRI must be valid"))
}
#[must_use]
pub fn create_task(&self, reference: &'a RiReferenceStr<S>) -> ResolutionTask<'a, S> {
let b = self.base_components;
let r = RiReferenceComponents::from(reference);
let (r_scheme, r_authority, r_path, r_query, r_fragment) = r.to_major();
let (b_scheme, b_authority, b_path, b_query, _) = b.to_major();
let b_scheme = b_scheme.expect("[validity] non-relative IRI must have a scheme");
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum RefToplevel {
Scheme,
Authority,
Path,
Query,
None,
}
impl RefToplevel {
fn choose<T>(self, component: RefToplevel, reference: T, base: T) -> T {
if self <= component {
reference
} else {
base
}
}
}
let ref_toplevel = if r_scheme.is_some() {
RefToplevel::Scheme
} else if r_authority.is_some() {
RefToplevel::Authority
} else if !r_path.is_empty() {
RefToplevel::Path
} else if r_query.is_some() {
RefToplevel::Query
} else {
RefToplevel::None
};
let path = match ref_toplevel {
RefToplevel::Scheme | RefToplevel::Authority => {
Path::NeedsDotSegRemoval(RemoveDotSegPath::from_single_path(r_path))
}
RefToplevel::Path => {
if r_path.starts_with('/') {
Path::NeedsDotSegRemoval(RemoveDotSegPath::from_single_path(r_path))
} else {
let b_path = if b_authority.is_some() && b_path.is_empty() {
"/"
} else {
b_path
};
Path::NeedsDotSegRemoval(RemoveDotSegPath::from_paths_to_be_resolved(
b_path, r_path,
))
}
}
RefToplevel::Query | RefToplevel::None => Path::Done(b_path),
};
ResolutionTask {
common: ResolutionTaskCommon {
scheme: r_scheme.unwrap_or(b_scheme),
authority: ref_toplevel.choose(RefToplevel::Authority, r_authority, b_authority),
path,
query: ref_toplevel.choose(RefToplevel::Query, r_query, b_query),
fragment: r_fragment,
},
_spec: PhantomData,
}
}
}
pub struct ResolutionTask<'a, S> {
common: ResolutionTaskCommon<'a>,
_spec: PhantomData<fn() -> S>,
}
impl<S: Spec> ResolutionTask<'_, S> {
fn write_to_buf<'b, B: Buffer<'b>>(&self, buf: B) -> Result<&'b [u8], Error>
where
ErrorRepr: From<B::ExtendError>,
{
self.common.write_to_buf(buf).map_err(Into::into)
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn allocate_and_write(&self) -> Result<RiString<S>, Error> {
let mut s = String::new();
self.write_to_buf(&mut s)?;
Ok(RiString::try_from(s).expect("[consistency] the resolved IRI must be valid"))
}
pub fn write_to_byte_slice<'b>(&self, buf: &'b mut [u8]) -> Result<&'b RiStr<S>, Error> {
let buf = ByteSliceBuf::new(buf);
let s = self.write_to_buf(buf)?;
let s = <&RiStr<S>>::try_from(s).expect("[consistency] the resolved IRI must be valid");
Ok(s)
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn write_to_iri_string(&self, dest: RiString<S>) -> Result<RiString<S>, Error> {
let mut buf: String = dest.into();
buf.clear();
self.write_to_buf(&mut buf)?;
Ok(RiString::<S>::try_from(buf).expect("[consistency] the resolved IRI must be valid"))
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn append_to_std_string<'b>(&self, buf: &'b mut String) -> Result<&'b RiStr<S>, Error> {
self.try_append_to_std_string(buf)
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn try_append_to_std_string<'b>(&self, buf: &'b mut String) -> Result<&'b RiStr<S>, Error> {
let s = self.write_to_buf(buf)?;
let s = <&RiStr<S>>::try_from(s).expect("[consistency] the resolved IRI must be valid");
Ok(s)
}
#[must_use]
pub fn estimate_max_buf_size_for_resolution(&self) -> usize {
let known_exact = self.common.scheme.len()
+ self.common.authority.map_or(0, |s| s.len() + 2)
+ self.common.query.map_or(0, |s| s.len() + 1)
+ self.common.fragment.map_or(0, |s| s.len() + 1);
let path_max = self.common.path.estimate_max_buf_size_for_resolution();
known_exact + path_max
}
}
struct ResolutionTaskCommon<'a> {
scheme: &'a str,
authority: Option<&'a str>,
path: Path<'a>,
query: Option<&'a str>,
fragment: Option<&'a str>,
}
impl ResolutionTaskCommon<'_> {
fn write_to_buf<'b, B: Buffer<'b>>(&self, mut buf: B) -> Result<&'b [u8], ErrorRepr>
where
ErrorRepr: From<B::ExtendError>,
{
buf.push_str(self.scheme)?;
buf.push_str(":")?;
buf.push_optional_with_prefix("//", self.authority)?;
let path_start_pos = buf.as_bytes().len();
match self.path {
Path::Done(s) => {
buf.push_str(s)?;
}
Path::NeedsDotSegRemoval(path) => {
path.merge_and_remove_dot_segments(&mut buf)?;
}
}
if self.authority.is_none() && buf.as_bytes()[path_start_pos..].starts_with(b"//") {
return Err(ErrorRepr::Unresolvable);
}
buf.push_optional_with_prefix("?", self.query)?;
buf.push_optional_with_prefix("#", self.fragment)?;
Ok(buf.into_bytes())
}
}
#[derive(Clone, Copy)]
enum Path<'a> {
Done(&'a str),
NeedsDotSegRemoval(RemoveDotSegPath<'a>),
}
impl Path<'_> {
#[must_use]
fn estimate_max_buf_size_for_resolution(&self) -> usize {
match self {
Self::Done(s) => s.len(),
Self::NeedsDotSegRemoval(path) => path.estimate_max_buf_size_for_resolution(),
}
}
}