use qualname::{NamespaceUri, NcName, QName};
use std::fs;
use std::path::Path;
use std::rc::Rc;
use std::sync::LazyLock;
use tokio::runtime::Runtime;
use url::Url;
use xrust::item::{Item, Node};
use xrust::security::{Policy, SecurityResult};
use xrust::transform::Transform;
use xrust::transform::callable::ActualParameters;
use xrust::value::Value;
use xrust::xdmerror::{Error, ErrorKind};
pub fn resolve<N: Node>(url: &Url, policy: &Rc<Policy<N>>) -> Result<String, Error> {
let rt = Runtime::new().unwrap();
rt.block_on(resolve_internal(url, policy))
}
async fn resolve_internal<N: Node>(url: &Url, policy: &Rc<Policy<N>>) -> Result<String, Error> {
if policy.get(
&*ACCESS_QNAME,
ActualParameters::Named(vec![
(
PARAM_URL.clone(),
Transform::Literal(Item::Value(Rc::new(Value::from(url.to_string())))),
), ]),
)? == SecurityResult::NotPermitted
{
return Err(Error::new(
ErrorKind::NotPermitted,
"resolving URL not permitted by security policy",
));
}
let limit = match policy.get(
&*MAXSIZE_QNAME,
ActualParameters::Named(vec![
(
PARAM_URL.clone(),
Transform::Literal(Item::Value(Rc::new(Value::from(url.to_string())))),
), ]),
)? {
SecurityResult::NotPermitted => {
return Err(Error::new(
ErrorKind::NotPermitted,
"access to URL not permitted by security policy",
));
}
SecurityResult::Permitted(None) => None,
SecurityResult::Permitted(Some(v)) => {
if let Ok(i) = v.parse::<usize>() {
Some(i)
} else {
return Err(Error::new(
ErrorKind::NotPermitted,
"security policy permits invalid value",
));
}
}
};
match url.scheme() {
"http" => {
let mut body = String::new();
let mut res = reqwest::get(url.to_string())
.await
.map_err(|e| Error::new(ErrorKind::Unknown, e.to_string()))?;
while let Some(chunk) = res
.chunk()
.await
.map_err(|e| Error::new(ErrorKind::Unknown, e.to_string()))?
{
if limit.is_some_and(|v| body.len() + chunk.len() > v) {
return Err(Error::new(
ErrorKind::NotPermitted,
format!("resource exceeds allowed length \"{}\"", limit.unwrap()),
));
} else {
let frag = chunk.utf8_chunks().try_fold(String::new(), |mut acc, c| {
acc.push_str(c.valid());
if !c.invalid().is_empty() {
Err(Error::new(ErrorKind::ParseError, "invalid UTF-8 character"))
} else {
Ok(acc)
}
})?;
body.push_str(frag.as_str())
}
}
Ok(body)
}
"file" => {
let body = fs::read_to_string(Path::new(url.path()))
.map_err(|er| Error::new(ErrorKind::Unknown, er.to_string()))?;
if limit.is_some_and(|v| body.len() > v) {
Err(Error::new(
ErrorKind::NotPermitted,
format!("resource exceeds allowed length \"{}\"", limit.unwrap()),
))
} else {
Ok(body)
}
}
_ => Err(Error::new(
ErrorKind::Unknown,
format!("unable to fetch URL \"{}\"", url.to_string()),
)),
}
}
static SECURITYNS: LazyLock<Option<NamespaceUri>> = LazyLock::new(|| {
Some(
NamespaceUri::try_from("http://gitlab.gnome.org/World/Rust/markup-rs/xrust-net/security")
.unwrap(),
)
});
static ACCESS_QNAME: LazyLock<QName> = LazyLock::new(|| {
QName::new_from_parts(NcName::try_from("access").unwrap(), SECURITYNS.clone())
});
static MAXSIZE_QNAME: LazyLock<QName> = LazyLock::new(|| {
QName::new_from_parts(
NcName::try_from("maximum-size").unwrap(),
SECURITYNS.clone(),
)
});
static PARAM_URL: LazyLock<QName> =
LazyLock::new(|| QName::new_from_parts(NcName::try_from("url").unwrap(), None));
#[cfg(test)]
mod tests {
use super::*;
use xrust::item::{Item, Node, SequenceTrait};
use xrust::parser::ParseError;
use xrust::parser::xml::parse as xmlparse;
use xrust::security::{Feature, Policy, SecurityPolicy};
use xrust::transform::callable::FormalParameters;
use xrust::transform::context::StaticContextBuilder;
use xrust::trees::smite::RNode;
use xrust::xslt::from_document;
fn make_from_str(s: &str) -> Result<RNode, Error> {
let d = RNode::new_document();
let _ = xmlparse(d.clone(), s, Some(|_: &_| Err(ParseError::Notimplemented)))?;
Ok(d)
}
#[test]
fn include() {
let mut policy: Policy<RNode> = Policy::new(QName::from_local_name(
NcName::try_from("test_policy").unwrap(),
));
policy.add(
QName::new_from_parts(
NcName::try_from("access").unwrap(),
Some(
NamespaceUri::try_from(
"http://gitlab.gnome.org/World/Rust/markup-rs/xrust-net/security",
)
.unwrap(),
),
),
Feature::new(
Transform::LiteralElement(
QName::new_from_parts(
NcName::try_from("permitted").unwrap(),
Some(
NamespaceUri::try_from(
"http://gitlab.gnome.org/World/Rust/markup-rs/Security",
)
.unwrap(),
),
),
Box::new(Transform::<RNode>::Empty),
),
FormalParameters::Named(vec![(PARAM_URL.clone(), None)]),
),
);
policy.add(
QName::new_from_parts(
NcName::try_from("maximum-size").unwrap(),
Some(
NamespaceUri::try_from(
"http://gitlab.gnome.org/World/Rust/markup-rs/xrust-net/security",
)
.unwrap(),
),
),
Feature::new(
Transform::LiteralElement(
QName::new_from_parts(
NcName::try_from("permitted").unwrap(),
Some(
NamespaceUri::try_from(
"http://gitlab.gnome.org/World/Rust/markup-rs/Security",
)
.unwrap(),
),
),
Box::new(Transform::<RNode>::Empty),
),
FormalParameters::Named(vec![(PARAM_URL.clone(), None)]),
),
);
let rpolicy = Rc::new(policy);
let mut sc = StaticContextBuilder::new()
.message(|_| Ok(()))
.fetcher(|u| resolve(u, &rpolicy))
.parser(|s| make_from_str(s))
.build();
let src = Item::Node(
make_from_str("<Test>one<Level1/>two<Level2/>three<Level3/>four<Level4/></Test>")
.expect("unable to parse source document"),
);
let style = make_from_str(
"<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
<xsl:include href='included.xsl'/>
<xsl:template match='child::Test'><xsl:apply-templates/></xsl:template>
<xsl:template match='child::Level1'>found Level1 element</xsl:template>
<xsl:template match='child::text()'><xsl:sequence select='.'/></xsl:template>
</xsl:stylesheet>",
)
.expect("unable to parse stylesheet");
let pwd = std::env::current_dir().expect("unable to get current directory");
let pwds = pwd
.into_os_string()
.into_string()
.expect("unable to convert pwd");
let mut ctxt = from_document(
style,
Some(
Url::parse(format!("file://{}/tests/xsl/including.xsl", pwds.as_str()).as_str())
.expect("unable to parse URL"),
),
|s| make_from_str(s),
|url| resolve(url, &rpolicy),
)
.expect("failed to compile stylesheet");
let rd = RNode::new_document();
ctxt.context(vec![src], 0);
ctxt.result_document(rd.clone());
let seq = ctxt.evaluate(&mut sc).expect("evaluation failed");
assert_eq!(
seq.to_xml(),
"onefound Level1 elementtwofound Level2 elementthreefound Level3 elementfour"
)
}
#[test]
fn local_only() {
let poldoc = make_from_str(r##"<sec:policy name='localonly' xmlns:sec='http://gitlab.gnome.org/World/Rust/markup-rs/Security'
xmlns:net='http://gitlab.gnome.org/World/Rust/markup-rs/xrust-net/security'
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
<sec:feature name='net:access'>
<xsl:param name='url'/>
<xsl:choose>
<xsl:when test='starts-with($url, "file://")'>
<sec:permitted/>
</xsl:when>
<xsl:otherwise>
<sec:not-permitted/>
</xsl:otherwise>
</xsl:choose>
</sec:feature>
<sec:feature name='net:maximum-size'>
<sec:permitted/>
</sec:feature>
</sec:policy>"##).expect("unable to parse security policy");
let policy = poldoc
.to_policy()
.expect("unable to compile security policy");
let rpolicy = Rc::new(policy);
let mut sc = StaticContextBuilder::new()
.message(|_| Ok(()))
.fetcher(|u| resolve(u, &rpolicy))
.parser(|s| make_from_str(s))
.build();
let src = Item::Node(
make_from_str("<Test>one<Level1/>two<Level2/>three<Level3/>four<Level4/></Test>")
.expect("unable to parse source document"),
);
let style1 = make_from_str(
"<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
<xsl:include href='included.xsl'/>
<xsl:template match='child::Test'><xsl:apply-templates/></xsl:template>
<xsl:template match='child::Level1'>found Level1 element</xsl:template>
<xsl:template match='child::text()'><xsl:sequence select='.'/></xsl:template>
</xsl:stylesheet>",
)
.expect("unable to parse stylesheet");
let pwd = std::env::current_dir().expect("unable to get current directory");
let pwds = pwd
.into_os_string()
.into_string()
.expect("unable to convert pwd");
let mut ctxt1 = from_document(
style1,
Some(
Url::parse(format!("file://{}/tests/xsl/including.xsl", pwds.as_str()).as_str())
.expect("unable to parse URL"),
),
|s| make_from_str(s),
|url| resolve(url, &rpolicy),
)
.expect("failed to compile stylesheet");
let rd1 = RNode::new_document();
ctxt1.context(vec![src.clone()], 0);
ctxt1.result_document(rd1.clone());
let seq1 = ctxt1.evaluate(&mut sc).expect("evaluation failed");
assert_eq!(
seq1.to_xml(),
"onefound Level1 elementtwofound Level2 elementthreefound Level3 elementfour"
);
let style2 = make_from_str(
"<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
<xsl:include href='http://example.org/xslt/included.xsl'/>
<xsl:template match='child::Test'><xsl:apply-templates/></xsl:template>
<xsl:template match='child::Level1'>found Level1 element</xsl:template>
<xsl:template match='child::text()'><xsl:sequence select='.'/></xsl:template>
</xsl:stylesheet>",
)
.expect("unable to parse stylesheet");
let result = from_document(
style2,
Some(
Url::parse("http://example.org/tests/xsl/including.xsl")
.expect("unable to parse URL"),
),
|s| make_from_str(s),
|url| resolve(url, &rpolicy),
);
assert!(result.is_err())
}
}