use std::path::{Path, PathBuf};
use crate::error::{AccessKind, CpeValidatorError, InputSource};
use crate::ffi::{self, Compiled, Denial, IoOperation, ReadPolicy, Session};
use crate::options::Limits;
pub struct Stylesheet {
style: ffi::Style,
path: Option<PathBuf>,
root: Option<PathBuf>,
max_file_bytes: u64,
}
impl std::fmt::Debug for Stylesheet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Stylesheet")
.field("path", &self.path)
.field("root", &self.root)
.finish_non_exhaustive()
}
}
fn canonical(path: &Path) -> Result<PathBuf, CpeValidatorError> {
std::fs::canonicalize(path).map_err(|source| CpeValidatorError::RuleIo {
path: path.to_path_buf(),
source,
})
}
pub(crate) fn access_kind(operation: IoOperation) -> AccessKind {
match operation {
IoOperation::ReadFile => AccessKind::ReadFile,
IoOperation::WriteFile => AccessKind::WriteFile,
IoOperation::CreateDirectory => AccessKind::CreateDirectory,
IoOperation::ReadNetwork => AccessKind::ReadNetwork,
IoOperation::WriteNetwork => AccessKind::WriteNetwork,
}
}
pub(crate) fn denial_error(denial: &Denial) -> CpeValidatorError {
CpeValidatorError::AccessDenied {
kind: access_kind(denial.operation),
resource: denial.resource.clone(),
reason: denial.reason,
}
}
impl Stylesheet {
pub fn from_file(
path: impl AsRef<Path>,
rules_root: Option<&Path>,
limits: &Limits,
) -> Result<Self, CpeValidatorError> {
let requested = path.as_ref();
let path = canonical(requested)?;
let root = match rules_root {
Some(root) => canonical(root)?,
None => path
.parent()
.map_or_else(|| path.clone(), Path::to_path_buf),
};
if !path.starts_with(&root) {
return Err(CpeValidatorError::RuleOutsideRoot { path, root });
}
let meta = std::fs::metadata(&path).map_err(|source| CpeValidatorError::RuleIo {
path: path.clone(),
source,
})?;
if meta.len() > limits.max_stylesheet_bytes {
return Err(CpeValidatorError::TooLarge {
source: InputSource::Stylesheet(path),
limit: limits.max_stylesheet_bytes,
actual: meta.len(),
});
}
let bytes = std::fs::read(&path).map_err(|source| CpeValidatorError::RuleIo {
path: path.clone(),
source,
})?;
Self::compile(&bytes, path, Some(root), limits)
}
pub fn from_bytes(
xsl: impl AsRef<[u8]>,
rules_root: Option<&Path>,
limits: &Limits,
) -> Result<Self, CpeValidatorError> {
let bytes = xsl.as_ref();
let actual = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
let (root, base) = match rules_root {
Some(root) => {
let root = canonical(root)?;
let base = root.join("inline.xsl");
(Some(root), base)
}
None => (None, PathBuf::from("/inline.xsl")),
};
if actual > limits.max_stylesheet_bytes {
return Err(CpeValidatorError::TooLarge {
source: InputSource::Stylesheet(base),
limit: limits.max_stylesheet_bytes,
actual,
});
}
let mut sheet = Self::compile(bytes, base, root, limits)?;
sheet.path = None;
Ok(sheet)
}
fn compile(
bytes: &[u8],
path: PathBuf,
root: Option<PathBuf>,
limits: &Limits,
) -> Result<Self, CpeValidatorError> {
let policy = ReadPolicy {
root: root.clone(),
max_file_bytes: limits.max_stylesheet_bytes,
};
match ffi::compile_stylesheet(bytes, &path, &policy).map_err(CpeValidatorError::Engine)? {
Compiled::Style(style) => Ok(Self {
style,
path: Some(path),
root,
max_file_bytes: limits.max_stylesheet_bytes,
}),
Compiled::Dtd => Err(CpeValidatorError::DoctypeForbidden {
source: InputSource::Stylesheet(path),
}),
Compiled::Malformed(session) | Compiled::Failed(session) => {
Err(compile_error(session, path))
}
}
}
#[must_use]
pub fn path(&self) -> Option<&Path> {
self.path.as_deref()
}
#[must_use]
pub fn rules_root(&self) -> Option<&Path> {
self.root.as_deref()
}
pub(crate) fn style(&self) -> &ffi::Style {
&self.style
}
pub(crate) fn read_policy(&self) -> ReadPolicy {
ReadPolicy {
root: self.root.clone(),
max_file_bytes: self.max_file_bytes,
}
}
}
fn compile_error(session: Session, path: PathBuf) -> CpeValidatorError {
if let Some(denial) = session.denials.first() {
return denial_error(denial);
}
if let Some(uri) = session.rejected_dtd.first() {
return CpeValidatorError::DoctypeForbidden {
source: InputSource::Included(uri.clone()),
};
}
CpeValidatorError::StylesheetInvalid {
path: Some(path),
diagnostics: session.messages,
}
}