use std::ffi::CString;
use std::path::{Path, PathBuf};
use crate::entity::CpeError;
use crate::error::{CpeValidatorError, InputSource, LimitKind};
use crate::ffi::{self, TransformSpec};
use crate::options::Limits;
use crate::parser::{ErrorResultParser, ResultParser};
use crate::resolver::{RuleResolver, TypeResolver, XmlTypeResolver, XslPathResolver};
use crate::stylesheet::{Stylesheet, denial_error};
use crate::xml::XmlDocument;
pub trait Validator {
fn validate(
&self,
filename: &str,
document: &XmlDocument,
) -> Result<Vec<CpeError>, CpeValidatorError>;
}
pub trait StylesheetValidator {
fn validate_with(
&self,
stylesheet: &Stylesheet,
filename: &str,
document: &XmlDocument,
) -> Result<Vec<CpeError>, CpeValidatorError>;
}
#[derive(Debug)]
pub struct XslValidator<P = ErrorResultParser> {
parser: P,
stylesheet: Option<Stylesheet>,
limits: Limits,
}
impl Default for XslValidator {
fn default() -> Self {
Self::new(ErrorResultParser)
}
}
impl<P: ResultParser> XslValidator<P> {
#[must_use]
pub fn new(parser: P) -> Self {
Self {
parser,
stylesheet: None,
limits: Limits::default(),
}
}
#[must_use]
pub fn with_limits(mut self, limits: Limits) -> Self {
self.limits = limits;
self
}
pub fn set_xsl(&mut self, stylesheet: Stylesheet) {
self.stylesheet = Some(stylesheet);
}
#[must_use]
pub fn stylesheet(&self) -> Option<&Stylesheet> {
self.stylesheet.as_ref()
}
#[must_use]
pub fn limits(&self) -> &Limits {
&self.limits
}
}
impl<P: ResultParser> Validator for XslValidator<P> {
fn validate(
&self,
filename: &str,
document: &XmlDocument,
) -> Result<Vec<CpeError>, CpeValidatorError> {
let stylesheet = self
.stylesheet
.as_ref()
.ok_or(CpeValidatorError::XslNotSet)?;
self.validate_with(stylesheet, filename, document)
}
}
impl<P: ResultParser> StylesheetValidator for XslValidator<P> {
fn validate_with(
&self,
stylesheet: &Stylesheet,
filename: &str,
document: &XmlDocument,
) -> Result<Vec<CpeError>, CpeValidatorError> {
let filename = CString::new(filename).map_err(|_| CpeValidatorError::InvalidFilename)?;
let spec = TransformSpec {
filename: &filename,
op_limit: self.limits.xpath_operations,
match_limit: self.limits.pcre_backtrack_limit,
depth_limit: self.limits.pcre_recursion_limit,
};
let outcome = ffi::transform(
stylesheet.style(),
&document.doc,
&spec,
&stylesheet.read_policy(),
)
.map_err(CpeValidatorError::Engine)?;
let session = outcome.session;
let errors: Vec<CpeError> = session
.messages
.iter()
.filter_map(|m| self.parser.parse(m))
.collect();
if let Some(call) = session.extension {
return Err(CpeValidatorError::Extension {
class: call.class,
message: call.message,
});
}
if let Some(denial) = session.denials.first() {
return Err(denial_error(denial));
}
if let Some(uri) = session.rejected_dtd.first() {
return Err(CpeValidatorError::DoctypeForbidden {
source: InputSource::Included(uri.clone()),
});
}
if session.op_limit_hit {
return Err(CpeValidatorError::ResourceLimit {
kind: LimitKind::XPathOperations,
});
}
if session.regex_limits > 0 {
return Err(CpeValidatorError::ResourceLimit {
kind: LimitKind::Regex,
});
}
if let Some(failure) = session.regex_errors.into_iter().next() {
return Err(CpeValidatorError::Regex {
pattern: failure.pattern,
message: failure.message,
});
}
if session.template_depth_hit {
return Err(CpeValidatorError::ResourceLimit {
kind: LimitKind::TemplateDepth,
});
}
if outcome.state == 1 || session.runtime_error {
return Err(CpeValidatorError::Transform {
partial: errors,
diagnostics: session.messages,
});
}
Ok(errors)
}
}
#[derive(Debug)]
pub struct CpeValidator<T = XmlTypeResolver, R = XslPathResolver, V = XslValidator> {
type_resolver: T,
rule_resolver: R,
xsl_validator: V,
limits: Limits,
}
impl CpeValidator {
#[must_use]
pub fn new(rules_dir: impl Into<PathBuf>) -> Self {
Self::with_parts(
XmlTypeResolver,
XslPathResolver::new(rules_dir),
XslValidator::default(),
)
}
}
impl<T: TypeResolver, R: RuleResolver, V: StylesheetValidator> CpeValidator<T, R, V> {
#[must_use]
pub fn with_parts(type_resolver: T, rule_resolver: R, xsl_validator: V) -> Self {
Self {
type_resolver,
rule_resolver,
xsl_validator,
limits: Limits::default(),
}
}
#[must_use]
pub fn with_limits(mut self, limits: Limits) -> Self {
self.limits = limits;
self
}
pub fn validate_xml(
&self,
filename: &str,
xml: impl AsRef<[u8]>,
) -> Result<Vec<CpeError>, CpeValidatorError> {
let document = XmlDocument::parse_with_limits(xml, &self.limits)?;
self.validate(filename, &document)
}
pub fn rule_path(&self, document: &XmlDocument) -> Result<Option<PathBuf>, CpeValidatorError> {
Ok(self
.type_resolver
.resolve(document)?
.and_then(|t| self.rule_resolver.rule_path(t)))
}
fn rules_root(&self) -> Option<&Path> {
self.rule_resolver.rules_root()
}
}
impl<T: TypeResolver, R: RuleResolver, V: StylesheetValidator> Validator for CpeValidator<T, R, V> {
fn validate(
&self,
filename: &str,
document: &XmlDocument,
) -> Result<Vec<CpeError>, CpeValidatorError> {
let Some(document_type) = self.type_resolver.resolve(document)? else {
return Ok(vec![CpeError::unknown_rule()]);
};
let Some(path) = self.rule_resolver.rule_path(document_type) else {
return Ok(vec![CpeError::unknown_rule()]);
};
let stylesheet = Stylesheet::from_file(&path, self.rules_root(), &self.limits)?;
self.xsl_validator
.validate_with(&stylesheet, filename, document)
}
}