#[cfg(feature = "embedded")]
use crate::xml_input_shared as xml_sec_xml_input;
use super::Result;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ResourceIdentity(pub String);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ResolvePurpose {
Include,
Import,
Document,
XInclude,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolveRequest<'a> {
pub uri: &'a str,
pub base_uri: Option<&'a str>,
pub purpose: ResolvePurpose,
pub accept: Option<&'a str>,
pub accept_language: Option<&'a str>,
}
impl<'a> ResolveRequest<'a> {
pub(crate) const fn new(
uri: &'a str,
base_uri: Option<&'a str>,
purpose: ResolvePurpose,
) -> Self {
Self {
uri,
base_uri,
purpose,
accept: None,
accept_language: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedResource {
pub canonical_uri: String,
pub identity: ResourceIdentity,
pub bytes: Vec<u8>,
pub media_type: Option<String>,
pub encoding: Option<String>,
}
#[derive(Debug, Clone, Copy)]
pub enum ResourceAccess<'a> {
Request(ResolveRequest<'a>),
Retained {
request: ResolveRequest<'a>,
identity: &'a ResourceIdentity,
},
Dependency(&'a ResourceIdentity),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AccessDenialReason {
NotGranted,
Revoked,
Expired,
}
impl ResourceAccess<'_> {
#[must_use]
pub fn denied(self, reason: AccessDenialReason) -> super::Error {
let resource = match self {
Self::Request(request) | Self::Retained { request, .. } => request.uri,
Self::Dependency(identity) => &identity.0,
};
super::Error::ResourceAccessDenied {
resource: resource.into(),
reason,
}
}
}
pub trait Resolver: Send + Sync {
fn authorize(&self, access: ResourceAccess<'_>) -> Result<()>;
fn resolve(&self, request: ResolveRequest<'_>) -> Result<ResolvedResource>;
}
pub(crate) fn resolve_uri_reference(base: Option<&str>, reference: &str) -> Result<String> {
if let Ok(absolute) = url::Url::parse(reference) {
return Ok(absolute.to_string());
}
if let Some(base) = base {
if let Ok(base_url) = url::Url::parse(base) {
if let Ok(joined) = base_url.join(reference) {
return Ok(joined.to_string());
}
if base_url.cannot_be_a_base()
&& let Some(joined) = resolve_rootless_absolute_uri(&base_url, reference)
{
return Ok(joined);
}
}
if reference.is_empty() {
return Ok(base
.split_once('#')
.map_or(base, |(document, _)| document)
.to_owned());
}
if reference.starts_with('#') {
let document = base.split_once('#').map_or(base, |(document, _)| document);
return Ok(format!("{document}{reference}"));
}
if reference.starts_with('?') {
let document = base.split(['?', '#']).next().unwrap_or(base);
return Ok(format!("{document}{reference}"));
}
if reference.starts_with('/') {
return Ok(reference.to_owned());
}
let base_path = base.split(['?', '#']).next().unwrap_or(base);
if let Some((directory, _)) = base_path.rsplit_once('/') {
return Ok(format!("{directory}/{reference}"));
}
}
Ok(reference.to_owned())
}
fn resolve_rootless_absolute_uri(base: &url::Url, reference: &str) -> Option<String> {
let mut synthetic = url::Url::parse(&format!(
"{}:/{}",
base.scheme(),
base.path().strip_prefix('/').unwrap_or(base.path())
))
.ok()?;
synthetic.set_query(base.query());
synthetic.set_fragment(base.fragment());
let mut joined = synthetic.join(reference).ok()?.to_string();
if reference.starts_with('/') {
return Some(joined);
}
let synthetic_root = base.scheme().len() + 1;
debug_assert_eq!(joined.as_bytes().get(synthetic_root), Some(&b'/'));
joined.remove(synthetic_root);
Some(joined)
}
pub(crate) fn decode_resource(
bytes: &[u8],
explicit: Option<&str>,
parsed_xml: bool,
maximum_decoded_bytes: usize,
) -> std::result::Result<String, xml_sec_xml_input::Error> {
let decoded = if parsed_xml {
xml_sec_xml_input::decode_xml_bounded(bytes, explicit, maximum_decoded_bytes)
} else {
xml_sec_xml_input::decode_text_bounded(
bytes,
explicit.unwrap_or("UTF-8"),
maximum_decoded_bytes,
)
};
decoded.map(|value| value.into_owned())
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoResolver;
impl Resolver for NoResolver {
fn authorize(&self, access: ResourceAccess<'_>) -> Result<()> {
Err(access.denied(AccessDenialReason::NotGranted))
}
fn resolve(&self, request: ResolveRequest<'_>) -> Result<ResolvedResource> {
Err(ResourceAccess::Request(request).denied(AccessDenialReason::NotGranted))
}
}
#[cfg(all(test, not(feature = "embedded")))]
mod tests;