use std::collections::{BTreeMap, HashSet};
use std::io::Read;
use std::sync::Mutex;
use quick_xml::events::Event;
use crate::error::{Diagnostic, Error, Result};
use crate::resource::Relationship;
pub trait ReadSeek: std::io::Read + std::io::Seek {}
impl<T: std::io::Read + std::io::Seek> ReadSeek for T {}
pub(crate) struct Package {
archive: Mutex<zip::ZipArchive<Box<dyn ReadSeek + Send>>>,
overrides: BTreeMap<String, String>,
defaults: BTreeMap<String, String>,
entries: Vec<String>,
names: HashSet<String>,
sizes: BTreeMap<String, u64>,
}
impl Package {
pub(crate) fn open(source: Box<dyn ReadSeek + Send>, diagnostics: &mut Vec<Diagnostic>) -> Result<Self> {
let mut archive =
zip::ZipArchive::new(source).map_err(|e| Error::Package(format!("not a zip: {e}")))?;
let entries: Vec<String> = archive
.file_names()
.map(|name| name.map(|n| n.into_owned()))
.collect::<std::result::Result<_, _>>()
.map_err(|e| Error::Package(format!("a part name cannot be read: {e}")))?;
let mut sizes = BTreeMap::new();
for index in 0..archive.len() {
if let Ok(entry) = archive.by_index_raw(index) {
let name = entry
.name()
.map_err(|e| Error::Package(format!("a part name cannot be read: {e}")))?
.into_owned();
sizes.insert(name, entry.size());
}
}
let mut content_types = Vec::new();
match archive.by_name("[Content_Types].xml") {
Ok(mut part) => {
part.read_to_end(&mut content_types)?;
}
Err(_) => {
diagnostics.push(Diagnostic::warning(
"[Content_Types].xml",
"absent, though OPC makes it mandatory; parts will be typed from their root element",
));
}
}
let (overrides, defaults) = parse_content_types(&content_types, diagnostics);
let names: HashSet<String> = entries.iter().cloned().collect();
Ok(Self {
archive: Mutex::new(archive),
overrides,
defaults,
entries,
names,
sizes,
})
}
pub(crate) fn entries(&self) -> &[String] {
&self.entries
}
pub(crate) fn size(&self, part: &str) -> Option<u64> {
self.sizes.get(&normalize(part)).copied()
}
pub(crate) fn read(&self, part: &str) -> Result<Vec<u8>> {
let name = normalize(part);
let mut archive = self
.archive
.lock()
.expect("the package lock is never held across a panic");
let mut entry = archive
.by_name(&name)
.map_err(|_| Error::Package(format!("no part `{name}` in the package")))?;
const PREALLOCATION_CAP: usize = 8 * 1024 * 1024;
let mut bytes = Vec::with_capacity((entry.size() as usize).min(PREALLOCATION_CAP));
entry.read_to_end(&mut bytes)?;
Ok(bytes)
}
pub(crate) fn with_part<T>(&self, part: &str, f: impl FnOnce(&mut dyn Read) -> T) -> Result<T> {
let name = normalize(part);
let mut archive = self
.archive
.lock()
.expect("the package lock is never held across a panic");
let mut entry = archive
.by_name(&name)
.map_err(|_| Error::Package(format!("no part `{name}` in the package")))?;
Ok(f(&mut entry))
}
pub(crate) fn has(&self, part: &str) -> bool {
let name = normalize(part);
self.names.contains(&name)
}
pub(crate) fn content_type(&self, part: &str) -> Option<&str> {
let name = normalize(part);
if let Some(ct) = self.overrides.get(&name) {
return Some(ct.as_str());
}
let extension = name.rsplit_once('.').map(|(_, e)| e.to_ascii_lowercase())?;
self.defaults.get(&extension).map(String::as_str)
}
pub(crate) fn relationships_reporting(&self, part: &str) -> Result<(Vec<Relationship>, Vec<Diagnostic>)> {
let rels_part = rels_name_of(part);
if !self.has(&rels_part) {
return Ok((Vec::new(), Vec::new()));
}
let bytes = self.read(&rels_part)?;
Ok(parse_relationships_reporting(&bytes, &rels_part))
}
}
pub(crate) fn normalize(part: &str) -> String {
part.trim_start_matches('/').to_string()
}
pub(crate) fn rels_name_of(part: &str) -> String {
let name = normalize(part);
match name.rsplit_once('/') {
Some((folder, file)) => format!("{folder}/_rels/{file}.rels"),
None if name.is_empty() => "_rels/.rels".to_string(),
None => format!("_rels/{name}.rels"),
}
}
fn parse_content_types(
xml: &[u8],
diagnostics: &mut Vec<Diagnostic>,
) -> (BTreeMap<String, String>, BTreeMap<String, String>) {
let mut overrides: BTreeMap<String, String> = BTreeMap::new();
let mut defaults: BTreeMap<String, String> = BTreeMap::new();
let mut reader = quick_xml::Reader::from_reader(xml);
let mut buf = Vec::new();
loop {
let event = match reader.read_event_into(&mut buf) {
Ok(event) => event,
Err(e) => {
diagnostics.push(Diagnostic::warning(
"[Content_Types].xml",
format!("unreadable: {e}"),
));
break;
}
};
match event {
Event::Eof => break,
Event::Start(e) | Event::Empty(e) => {
let local = local_name(e.name().as_ref());
let (mut key, mut value) = (None, None);
for attribute in e.attributes().flatten() {
let text = String::from_utf8_lossy(&attribute.value).into_owned();
match attribute.key.as_ref() {
b"PartName" | b"Extension" => key = Some(text),
b"ContentType" => value = Some(text),
_ => {}
}
}
match (local.as_str(), key, value) {
("Override", Some(part), Some(ct)) => {
let part = normalize(&part);
if let Some(previous) = overrides.insert(part.clone(), ct.clone())
&& previous != ct
{
diagnostics.push(Diagnostic::warning(
part,
format!("declared twice, as `{previous}` and `{ct}`; the last wins"),
));
}
}
("Default", Some(extension), Some(ct)) => {
defaults.insert(extension.to_ascii_lowercase(), ct);
}
_ => {}
}
}
_ => {}
}
buf.clear();
}
(overrides, defaults)
}
pub(crate) fn parse_relationships_reporting(xml: &[u8], part: &str) -> (Vec<Relationship>, Vec<Diagnostic>) {
let (out, diagnostics, _) = parse_relationships_counting(xml, part);
(out, diagnostics)
}
#[derive(Default)]
struct SeenIds {
ids: HashSet<String>,
probes: u64,
}
impl SeenIds {
fn repeats(&mut self, id: &str) -> bool {
self.probes += 1;
!self.ids.insert(id.to_string())
}
}
#[doc(hidden)]
pub fn parse_relationships_counting(xml: &[u8], part: &str) -> (Vec<Relationship>, Vec<Diagnostic>, u64) {
let mut seen = SeenIds::default();
let mut out: Vec<Relationship> = Vec::new();
let mut diagnostics = Vec::new();
let mut reader = quick_xml::Reader::from_reader(xml);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Eof) => break,
Err(e) => {
diagnostics.push(Diagnostic::warning(
part,
format!(
"unreadable from byte {}: {e}; the {} relationship(s) read before it are kept, \
anything after is lost",
reader.buffer_position(),
out.len()
),
));
break;
}
Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
if local_name(e.name().as_ref()) != "Relationship" {
buf.clear();
continue;
}
let (mut id, mut kind, mut target, mut mode) =
(String::new(), String::new(), String::new(), String::new());
for attribute in e.attributes().flatten() {
let text = String::from_utf8_lossy(&attribute.value).into_owned();
match attribute.key.as_ref() {
b"Id" => id = text,
b"Type" => kind = text,
b"Target" => target = text,
b"TargetMode" => mode = text,
_ => {}
}
}
if !id.is_empty() && seen.repeats(&id) {
diagnostics.push(Diagnostic::note(
part,
format!("`Id=\"{id}\"` appears more than once; the OPC schema makes it unique"),
));
}
if target.is_empty() {
diagnostics.push(Diagnostic::note(
part,
format!("a `Relationship` (Id=`{id}`) has an empty `Target`"),
));
}
out.push(Relationship {
id,
kind,
target,
external: mode.eq_ignore_ascii_case("external"),
});
}
_ => {}
}
buf.clear();
}
(out, diagnostics, seen.probes)
}
fn local_name(raw: &[u8]) -> String {
String::from_utf8_lossy(raw.rsplit(|b| *b == b':').next().unwrap_or_default()).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rels_names_follow_the_part() {
assert_eq!(rels_name_of("Foo.xml"), "_rels/Foo.xml.rels");
assert_eq!(rels_name_of("/Foo.xml"), "_rels/Foo.xml.rels");
assert_eq!(rels_name_of("resqml20/Foo.xml"), "resqml20/_rels/Foo.xml.rels");
assert_eq!(rels_name_of(""), "_rels/.rels");
}
#[test]
fn a_part_declared_twice_is_reported_and_the_last_wins() {
let xml = br#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Override PartName="/a.xml" ContentType="application/x-resqml+xml;version=2.0;type=obj_Activity"/>
<Override PartName="/a.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
</Types>"#;
let mut diagnostics = Vec::new();
let (overrides, defaults) = parse_content_types(xml, &mut diagnostics);
assert_eq!(
overrides.get("a.xml").unwrap(),
"application/vnd.openxmlformats-package.core-properties+xml"
);
assert_eq!(defaults.len(), 1);
assert_eq!(diagnostics.len(), 1, "{diagnostics:?}");
}
#[test]
fn an_external_relationship_is_recognised() {
let xml = br#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="Hdf5File" TargetMode="External" Target="model.h5"
Type="http://schemas.energistics.org/package/2012/relationships/externalResource"/>
<Relationship Id="_x" Target="Other.xml"
Type="http://schemas.energistics.org/package/2012/relationships/destinationObject"/>
</Relationships>"#;
let (rels, problems) = parse_relationships_reporting(xml, "_rels/a.xml.rels");
assert_eq!(rels.len(), 2);
assert!(rels[0].is_external_resource());
assert_eq!(rels[0].target, "model.h5");
assert_eq!(rels[1].short_kind(), "destinationObject");
assert!(!rels[1].is_external_resource());
assert!(problems.is_empty(), "{problems:?}");
}
#[test]
fn a_truncated_rels_keeps_what_it_read_and_says_what_it_lost() {
let xml = br#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="Hdf5File" TargetMode="External" Target="model.h5"
Type="http://schemas.energistics.org/package/2012/relationships/externalResource"/>
<Relationship Id="_x" Target="Other.xml" Type="htt"#;
let (rels, problems) = parse_relationships_reporting(xml, "_rels/a.xml.rels");
assert_eq!(rels.len(), 1, "what was read is kept");
assert_eq!(problems.len(), 1, "{problems:?}");
assert!(problems[0].message.contains("kept"), "{:?}", problems[0]);
}
#[test]
fn a_duplicate_id_is_reported() {
let xml = br#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="_x" Target="A.xml" Type="t"/>
<Relationship Id="_x" Target="B.xml" Type="t"/>
</Relationships>"#;
let (rels, problems) = parse_relationships_reporting(xml, "_rels/a.xml.rels");
assert_eq!(rels.len(), 2, "both are kept: the targets differ");
assert_eq!(problems.len(), 1, "{problems:?}");
assert!(problems[0].message.contains("more than once"));
}
}