use crate::Result;
use roxmltree::{Document, ParsingOptions};
use std::{collections::BTreeMap, fs::File, io::Read, path::Path};
const MAX_SOURCE: u64 = 64 * 1024 * 1024;
const MAX_ENTRIES: usize = 16_384;
pub(crate) const MAX_XML: usize = 16 * 1024 * 1024;
const MAX_DECODED: usize = 128 * 1024 * 1024;
const MAX_NODES: u32 = 1_000_000;
const MAX_DEPTH: usize = 64;
const MAX_STRING: usize = 1024 * 1024;
pub(crate) struct DocumentArchive {
archive: zip::ZipArchive<File>,
entries: BTreeMap<String, usize>,
document: String,
decoded: usize,
}
impl DocumentArchive {
pub(crate) fn open(path: &Path) -> Result<Self> {
let mut file = File::open(path)?;
let length = file.metadata()?.len();
if length > MAX_SOURCE {
return Err("FCStd source exceeds the 64 MiB limit".into());
}
let physical = super::zip_directory::validate(&mut file, length, MAX_ENTRIES)?;
let mut archive = zip::ZipArchive::new(file)?;
if archive.len() != physical {
return Err("FCStd duplicate decoded ZIP member name".into());
}
let mut entries = BTreeMap::new();
for index in 0..archive.len() {
let entry = archive.by_index(index)?;
let name = entry.name()?;
let canonical = member_name(&name, entry.is_dir())?;
if entries.insert(canonical, index).is_some() {
return Err("FCStd duplicate canonical ZIP member name".into());
}
}
let mut result = Self {
archive,
entries,
document: String::new(),
decoded: 0,
};
let bytes = result.read("Document.xml", MAX_XML)?;
result.document = String::from_utf8(bytes)?;
let document = xml(&result.document)?;
let root = document.root_element();
if root.tag_name().name() != "Document"
|| root.tag_name().namespace().is_some()
|| root.attribute("SchemaVersion") != Some("4")
|| root.attribute("FileVersion") != Some("1")
{
return Err("FCStd requires Document SchemaVersion=4 and FileVersion=1".into());
}
Ok(result)
}
pub(crate) fn document(&self) -> &str {
&self.document
}
pub(crate) fn read(&mut self, name: &str, limit: usize) -> Result<Vec<u8>> {
self.read_optional(name, limit)?
.ok_or_else(|| format!("FCStd addressed member is missing: {name}").into())
}
pub(crate) fn read_optional(&mut self, name: &str, limit: usize) -> Result<Option<Vec<u8>>> {
member_name(name, false)?;
let Some(&index) = self.entries.get(name) else {
return Ok(None);
};
let mut entry = self.archive.by_index(index)?;
if entry.is_dir() {
return Err("FCStd addressed member is a directory".into());
}
if !matches!(
entry.compression(),
zip::CompressionMethod::Stored | zip::CompressionMethod::Deflated
) {
return Err("FCStd selected ZIP member compression is unsupported".into());
}
let limit = limit.min(MAX_DECODED.saturating_sub(self.decoded));
if entry.size() > u64::try_from(limit)? {
return Err("FCStd selected member exceeds decoded-byte limit".into());
}
let capacity = usize::try_from(entry.size())?;
let mut bytes = Vec::with_capacity(capacity);
entry
.by_ref()
.take(u64::try_from(limit)? + 1)
.read_to_end(&mut bytes)?;
if bytes.len() > limit || bytes.len() != capacity {
return Err("FCStd selected member has invalid decoded size".into());
}
self.decoded = self
.decoded
.checked_add(bytes.len())
.ok_or("FCStd decoded-byte count overflow")?;
Ok(Some(bytes))
}
}
fn member_name(name: &str, directory: bool) -> Result<String> {
let canonical = if directory {
name.strip_suffix('/').unwrap_or(name)
} else {
name
};
if canonical.is_empty()
|| canonical.len() > 4096
|| canonical.contains(['\\', '\0', ':'])
|| canonical
.split('/')
.any(|part| part.is_empty() || matches!(part, "." | ".."))
{
return Err("FCStd ZIP member has an invalid or ambiguous path".into());
}
Ok(canonical.to_owned())
}
pub(crate) fn xml(text: &str) -> Result<Document<'_>> {
if text.len() > MAX_XML {
return Err("FCStd XML exceeds the 16 MiB limit".into());
}
let document = Document::parse_with_options(
text,
ParsingOptions {
allow_dtd: false,
nodes_limit: MAX_NODES,
..ParsingOptions::default()
},
)
.map_err(|error| format!("FCStd native XML document is invalid: {error}"))?;
for node in document.descendants().filter(roxmltree::Node::is_element) {
if node.tag_name().namespace().is_some()
|| node
.attributes()
.any(|attribute| attribute.namespace().is_some())
{
return Err("FCStd namespaced native XML carriers are unsupported".into());
}
if node.ancestors().take(MAX_DEPTH + 2).count() > MAX_DEPTH + 1
|| node.tag_name().name().len() > MAX_STRING
|| node.attributes().len() > 64
|| node.attributes().any(|attribute| {
attribute.name().len() > MAX_STRING || attribute.value().len() > MAX_STRING
})
{
return Err("FCStd XML exceeds nesting or attribute limits".into());
}
}
Ok(document)
}