use std::collections::BTreeMap;
use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;
use super::content_types::{ContentTypes, DefaultExt};
use super::part::{Part, PartName};
use super::rels::Relationship;
pub mod ct {
pub const RELATIONSHIPS: &str = "application/vnd.openxmlformats-package.relationships+xml";
pub const THEME: &str = "application/vnd.openxmlformats-officedocument.theme+xml";
pub const CORE_PROPS: &str = "application/vnd.openxmlformats-package.core-properties+xml";
pub const APP_PROPS: &str =
"application/vnd.openxmlformats-officedocument.extended-properties+xml";
pub const CUSTOM_PROPS: &str =
"application/vnd.openxmlformats-officedocument.custom-properties+xml";
pub const CHART: &str = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml";
pub const OLE_OBJECT: &str = "application/vnd.openxmlformats-officedocument.oleobject";
pub const SPREADSHEET_XLSX: &str =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
pub const VIDEO_MP4: &str = "video/mp4";
pub const AUDIO_MP3: &str = "audio/mp3";
pub const DIAGRAM_DATA: &str =
"application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml";
pub const DIAGRAM_LAYOUT: &str =
"application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml";
pub const DIAGRAM_QUICK_STYLE: &str =
"application/vnd.openxmlformats-officedocument.drawingml.diagramQuickStyle+xml";
pub const DIAGRAM_COLORS: &str =
"application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml";
}
#[derive(Debug, Clone, Default)]
pub struct OpcPackage {
pub parts: BTreeMap<String, Part>,
pub content_types: ContentTypes,
}
impl OpcPackage {
pub fn new() -> Self {
OpcPackage {
parts: BTreeMap::new(),
content_types: ContentTypes::new_default(),
}
}
pub fn put_part(&mut self, p: Part) {
p.contribute_to(&mut self.content_types);
self.parts.insert(p.partname.as_str().to_string(), p);
}
pub fn get_part(&self, partname: &str) -> Option<&Part> {
self.parts.get(partname)
}
pub fn get_part_mut(&mut self, partname: &str) -> Option<&mut Part> {
self.parts.get_mut(partname)
}
pub fn iter_parts(&self) -> impl Iterator<Item = &Part> {
self.parts.values()
}
pub fn part_count(&self) -> usize {
self.parts.len()
}
pub fn load(path: impl AsRef<Path>) -> crate::Result<Self> {
let file = File::open(path.as_ref())?;
let mut zip = zip::ZipArchive::new(file)?;
let mut pkg = OpcPackage::new();
let mut ct_xml = String::new();
zip.by_name("[Content_Types].xml")?
.read_to_string(&mut ct_xml)?;
pkg.content_types = parse_content_types_public(&ct_xml)?;
for i in 0..zip.len() {
let mut entry = zip.by_index(i)?;
let name = entry.name().to_string();
if name == "[Content_Types].xml" {
continue;
}
if entry.is_dir() {
continue;
}
let mut blob = Vec::with_capacity(entry.size() as usize);
entry.read_to_end(&mut blob)?;
let ct = if name.ends_with(".rels") {
ct::RELATIONSHIPS.to_string()
} else {
derive_content_type(&pkg.content_types, &format!("/{}", name))
};
let partname = format!("/{}", name);
let part = Part::new(PartName::from_unchecked(partname), ct, blob);
pkg.parts.insert(part.partname.as_str().to_string(), part);
}
Ok(pkg)
}
pub fn save(&self, path: impl AsRef<Path>) -> crate::Result<()> {
let file = File::create(path.as_ref())?;
let mut zip = zip::ZipWriter::new(file);
let opts: zip::write::FileOptions<'_, ()> = zip::write::FileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.unix_permissions(0o644);
zip.start_file("[Content_Types].xml", opts)?;
zip.write_all(self.content_types.to_xml().as_bytes())?;
for part in self.parts.values() {
let p = part.partname.to_zip_path();
zip.start_file(p, opts)?;
zip.write_all(&part.blob)?;
}
zip.finish()?;
Ok(())
}
pub fn to_bytes(&self) -> crate::Result<Vec<u8>> {
use std::io::Cursor;
let mut buf: Vec<u8> = Vec::new();
{
let cursor = Cursor::new(&mut buf);
let mut zip = zip::ZipWriter::new(cursor);
let opts: zip::write::FileOptions<'_, ()> = zip::write::FileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
zip.start_file("[Content_Types].xml", opts)?;
zip.write_all(self.content_types.to_xml().as_bytes())?;
for part in self.parts.values() {
zip.start_file(part.partname.to_zip_path(), opts)?;
zip.write_all(&part.blob)?;
}
zip.finish()?;
}
Ok(buf)
}
}
pub fn derive_content_type(ct: &ContentTypes, partname: &str) -> String {
for o in &ct.overrides {
if o.partname == partname {
return o.content_type.clone();
}
}
let ext = match partname.rfind('.') {
Some(i) => &partname[i + 1..],
None => "",
};
for d in &ct.defaults {
if d.extension == ext {
return d.content_type.clone();
}
}
"application/octet-stream".to_string()
}
pub fn parse_content_types_public(xml: &str) -> crate::Result<ContentTypes> {
use quick_xml::events::Event;
use quick_xml::reader::Reader;
let mut ct = ContentTypes::new_default();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) => {
let name = e.name();
match name.as_ref() {
b"Default" => {
let mut ext = None;
let mut ctype = None;
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"Extension" => {
ext = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.ok()
.map(|v| v.to_string())
}
b"ContentType" => {
ctype = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.ok()
.map(|v| v.to_string())
}
_ => {}
}
}
if let (Some(ext), Some(ctype)) = (ext, ctype) {
ct.defaults.push(DefaultExt::new(ext, ctype));
}
}
b"Override" => {
let mut partname = None;
let mut ctype = None;
for a in e.attributes().flatten() {
match a.key.as_ref() {
b"PartName" => {
partname = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.ok()
.map(|v| v.to_string())
}
b"ContentType" => {
ctype = a
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.ok()
.map(|v| v.to_string())
}
_ => {}
}
}
if let (Some(p), Some(c)) = (partname, ctype) {
ct.add_override(&p, &c);
}
}
_ => {}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("content-types parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(ct)
}
pub fn rels_partname_for(partname: &str) -> String {
let p = partname.trim_start_matches('/');
let last_slash = p.rfind('/');
match last_slash {
None => format!("/_rels/{}.rels", p),
Some(i) => format!("/{}/_rels/{}.rels", &p[..i], &p[i + 1..]),
}
}
pub fn next_rid(existing: &[Relationship], prefix: &str) -> String {
let mut max = 0u32;
for r in existing {
if let Some(n) = r.id.strip_prefix(prefix) {
if let Ok(v) = n.parse::<u32>() {
if v > max {
max = v;
}
}
}
}
format!("{}{}", prefix, max + 1)
}