use std::fs;
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::{DocxError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InputLimits {
pub max_docx_archive_bytes: u64,
pub max_docx_entries: usize,
pub max_docx_entry_bytes: u64,
pub max_docx_total_bytes: u64,
pub max_zip_compression_ratio: u64,
pub max_xml_bytes: u64,
pub max_spec_bytes: u64,
pub max_include_depth: usize,
pub max_include_files: usize,
pub max_image_bytes: u64,
pub max_svg_bytes: u64,
pub max_image_pixels: u64,
pub max_template_expansions: usize,
pub max_template_output_xml_bytes: u64,
pub max_template_partial_depth: usize,
}
impl Default for InputLimits {
fn default() -> Self {
Self {
max_docx_archive_bytes: 64 * 1024 * 1024,
max_docx_entries: 4_096,
max_docx_entry_bytes: 64 * 1024 * 1024,
max_docx_total_bytes: 256 * 1024 * 1024,
max_zip_compression_ratio: 200,
max_xml_bytes: 16 * 1024 * 1024,
max_spec_bytes: 8 * 1024 * 1024,
max_include_depth: 32,
max_include_files: 128,
max_image_bytes: 32 * 1024 * 1024,
max_svg_bytes: 8 * 1024 * 1024,
max_image_pixels: 64_000_000,
max_template_expansions: 100_000,
max_template_output_xml_bytes: 64 * 1024 * 1024,
max_template_partial_depth: 32,
}
}
}
impl InputLimits {
pub const fn hosted() -> Self {
Self {
max_docx_archive_bytes: 16 * 1024 * 1024,
max_docx_entries: 1_024,
max_docx_entry_bytes: 16 * 1024 * 1024,
max_docx_total_bytes: 64 * 1024 * 1024,
max_zip_compression_ratio: 100,
max_xml_bytes: 4 * 1024 * 1024,
max_spec_bytes: 2 * 1024 * 1024,
max_include_depth: 12,
max_include_files: 32,
max_image_bytes: 8 * 1024 * 1024,
max_svg_bytes: 2 * 1024 * 1024,
max_image_pixels: 16_000_000,
max_template_expansions: 10_000,
max_template_output_xml_bytes: 8 * 1024 * 1024,
max_template_partial_depth: 12,
}
}
pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let content = fs::read_to_string(path)?;
let extension = path
.extension()
.and_then(|value| value.to_str())
.unwrap_or("toml")
.to_ascii_lowercase();
let limits: Self = match extension.as_str() {
"toml" | "" => toml::from_str(&content)
.map_err(|error| DocxError::parse(format!("invalid TOML limits: {error}")))?,
"json" => serde_json::from_str(&content)
.map_err(|error| DocxError::parse(format!("invalid JSON limits: {error}")))?,
other => {
return Err(DocxError::parse(format!(
"unsupported limits extension '{other}', expected .toml or .json"
)))
}
};
limits.validate()?;
Ok(limits)
}
pub fn validate(&self) -> Result<()> {
let positive = [
("max_docx_archive_bytes", self.max_docx_archive_bytes),
("max_docx_entry_bytes", self.max_docx_entry_bytes),
("max_docx_total_bytes", self.max_docx_total_bytes),
("max_zip_compression_ratio", self.max_zip_compression_ratio),
("max_xml_bytes", self.max_xml_bytes),
("max_spec_bytes", self.max_spec_bytes),
("max_image_bytes", self.max_image_bytes),
("max_svg_bytes", self.max_svg_bytes),
("max_image_pixels", self.max_image_pixels),
(
"max_template_output_xml_bytes",
self.max_template_output_xml_bytes,
),
];
if let Some((name, _)) = positive.into_iter().find(|(_, value)| *value == 0) {
return Err(DocxError::resource_limit(format!(
"{name} must be greater than zero"
)));
}
let counts = [
("max_docx_entries", self.max_docx_entries),
("max_include_depth", self.max_include_depth),
("max_include_files", self.max_include_files),
("max_template_expansions", self.max_template_expansions),
(
"max_template_partial_depth",
self.max_template_partial_depth,
),
];
if let Some((name, _)) = counts.into_iter().find(|(_, value)| *value == 0) {
return Err(DocxError::resource_limit(format!(
"{name} must be greater than zero"
)));
}
if self.max_docx_entry_bytes > self.max_docx_total_bytes {
return Err(DocxError::resource_limit(
"max_docx_entry_bytes cannot exceed max_docx_total_bytes".to_string(),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::InputLimits;
#[test]
fn hosted_profile_is_stricter_and_valid() {
let hosted = InputLimits::hosted();
let default = InputLimits::default();
hosted.validate().expect("hosted profile");
assert!(hosted.max_docx_total_bytes < default.max_docx_total_bytes);
assert!(hosted.max_image_pixels < default.max_image_pixels);
}
#[test]
fn invalid_profiles_fail_closed() {
let invalid = InputLimits {
max_spec_bytes: 0,
..InputLimits::default()
};
assert!(invalid.validate().is_err());
}
#[test]
fn serialized_profiles_must_be_complete_and_known() {
assert!(toml::from_str::<InputLimits>("max_spec_bytes = 64").is_err());
let mut profile = toml::to_string(&InputLimits::hosted()).expect("serialize profile");
profile.push_str("unknown_budget = 1\n");
assert!(toml::from_str::<InputLimits>(&profile).is_err());
}
#[test]
fn checked_in_hosted_profile_matches_the_builtin() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/config/hosted-limits.toml");
assert_eq!(
InputLimits::load_from_path(path).expect("hosted limits fixture"),
InputLimits::hosted()
);
}
}