use crate::error::{Error, Result};
use crate::ext;
use crate::generated as types;
use crate::generated_serializers::ToXml;
use ooxml_opc::{Package, PackageWriter, Relationships, rel_type, rels_path_for};
use ooxml_xml::{PositionedNode, RawXmlElement, RawXmlNode};
use quick_xml::Reader;
use quick_xml::events::Event;
use std::fs::File;
use std::io::{BufReader, Read, Seek};
use std::path::Path;
pub struct Document<R> {
package: Package<R>,
gen_doc: types::Document,
gen_styles: types::Styles,
doc_rels: Relationships,
doc_path: String,
styles_path: Option<String>,
core_properties: Option<CoreProperties>,
app_properties: Option<AppProperties>,
}
impl Document<BufReader<File>> {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let file = File::open(path)?;
let reader = BufReader::new(file);
Self::from_reader(reader)
}
}
impl<R: Read + Seek> Document<R> {
pub fn from_reader(reader: R) -> Result<Self> {
let mut package = Package::open(reader)?;
let rels = package.read_relationships()?;
let doc_rel = rels
.get_by_type(rel_type::OFFICE_DOCUMENT)
.ok_or_else(|| Error::MissingPart("main document relationship".into()))?;
let doc_path = doc_rel.target.clone();
let doc_xml = package.read_part(&doc_path)?;
let gen_doc = ext::parse_document(&doc_xml)?;
let doc_rels_path = rels_path_for(&doc_path);
let doc_rels = if package.has_part(&doc_rels_path) {
let rels_xml = package.read_part(&doc_rels_path)?;
Relationships::parse(&rels_xml[..])?
} else {
Relationships::new()
};
let (gen_styles, styles_path) = if let Some(styles_rel) = rels.get_by_type(rel_type::STYLES)
{
let path = styles_rel.target.clone();
let styles_xml = package.read_part(&path)?;
(ext::parse_styles(&styles_xml)?, Some(path))
} else if let Some(styles_rel) = doc_rels.get_by_type(rel_type::STYLES) {
let path = resolve_path(&doc_path, &styles_rel.target);
let styles_xml = package.read_part(&path)?;
(ext::parse_styles(&styles_xml)?, Some(path))
} else {
(types::Styles::default(), None)
};
let core_properties = if let Some(core_rel) = rels.get_by_type(rel_type::CORE_PROPERTIES) {
let core_xml = package.read_part(&core_rel.target)?;
Some(parse_core_properties(&core_xml)?)
} else {
None
};
let app_properties = if let Some(app_rel) = rels.get_by_type(rel_type::EXTENDED_PROPERTIES)
{
let app_xml = package.read_part(&app_rel.target)?;
Some(parse_app_properties(&app_xml)?)
} else {
None
};
Ok(Self {
package,
gen_doc,
gen_styles,
doc_rels,
doc_path,
styles_path,
core_properties,
app_properties,
})
}
pub fn body(&self) -> &types::Body {
self.gen_doc
.body
.as_deref()
.expect("document has no body element")
}
pub fn body_mut(&mut self) -> &mut types::Body {
self.gen_doc
.body
.as_deref_mut()
.expect("document has no body element")
}
pub fn gen_doc(&self) -> &types::Document {
&self.gen_doc
}
pub fn package(&self) -> &Package<R> {
&self.package
}
pub fn package_mut(&mut self) -> &mut Package<R> {
&mut self.package
}
pub fn styles(&self) -> &types::Styles {
&self.gen_styles
}
pub fn core_properties(&self) -> Option<&CoreProperties> {
self.core_properties.as_ref()
}
pub fn app_properties(&self) -> Option<&AppProperties> {
self.app_properties.as_ref()
}
pub fn text(&self) -> String {
use crate::ext::BodyExt;
self.gen_doc
.body
.as_deref()
.map(|b| b.text())
.unwrap_or_default()
}
pub fn get_image_data(&mut self, rel_id: &str) -> Result<ImageData> {
let rel = self
.doc_rels
.get(rel_id)
.ok_or_else(|| Error::MissingPart(format!("image relationship {}", rel_id)))?;
let image_path = resolve_path(&self.doc_path, &rel.target);
let data = self.package.read_part(&image_path)?;
let content_type = content_type_from_path(&image_path);
Ok(ImageData { content_type, data })
}
pub fn get_hyperlink_url(&self, rel_id: &str) -> Option<&str> {
self.doc_rels.get(rel_id).map(|rel| rel.target.as_str())
}
pub fn doc_relationships(&self) -> &Relationships {
&self.doc_rels
}
pub fn get_header(&mut self, rel_id: &str) -> Result<types::HeaderFooter> {
let rel = self
.doc_rels
.get(rel_id)
.ok_or_else(|| Error::MissingPart(format!("header relationship {}", rel_id)))?;
let header_path = resolve_path(&self.doc_path, &rel.target);
let header_xml = self.package.read_part(&header_path)?;
Ok(ext::parse_hdr_ftr(&header_xml)?)
}
pub fn get_footer(&mut self, rel_id: &str) -> Result<types::HeaderFooter> {
let rel = self
.doc_rels
.get(rel_id)
.ok_or_else(|| Error::MissingPart(format!("footer relationship {}", rel_id)))?;
let footer_path = resolve_path(&self.doc_path, &rel.target);
let footer_xml = self.package.read_part(&footer_path)?;
Ok(ext::parse_hdr_ftr(&footer_xml)?)
}
pub fn get_footnotes(&mut self) -> Result<types::Footnotes> {
let footnotes_rel = self
.doc_rels
.get_by_type(
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes",
)
.ok_or_else(|| Error::MissingPart("footnotes relationship".into()))?;
let footnotes_path = resolve_path(&self.doc_path, &footnotes_rel.target);
let footnotes_xml = self.package.read_part(&footnotes_path)?;
Ok(ext::parse_footnotes(&footnotes_xml)?)
}
pub fn get_endnotes(&mut self) -> Result<types::Endnotes> {
let endnotes_rel = self
.doc_rels
.get_by_type(
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes",
)
.ok_or_else(|| Error::MissingPart("endnotes relationship".into()))?;
let endnotes_path = resolve_path(&self.doc_path, &endnotes_rel.target);
let endnotes_xml = self.package.read_part(&endnotes_path)?;
Ok(ext::parse_endnotes(&endnotes_xml)?)
}
pub fn get_comments(&mut self) -> Result<types::Comments> {
let comments_rel = self
.doc_rels
.get_by_type(
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments",
)
.ok_or_else(|| Error::MissingPart("comments relationship".into()))?;
let comments_path = resolve_path(&self.doc_path, &comments_rel.target);
let comments_xml = self.package.read_part(&comments_path)?;
Ok(ext::parse_comments(&comments_xml)?)
}
pub fn get_settings(&mut self) -> Result<DocumentSettings> {
let settings_rel = self
.doc_rels
.get_by_type(
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings",
)
.ok_or_else(|| Error::MissingPart("settings relationship".into()))?;
let settings_path = resolve_path(&self.doc_path, &settings_rel.target);
let settings_xml = self.package.read_part(&settings_path)?;
parse_settings(&settings_xml)
}
#[cfg(feature = "wml-charts")]
pub fn get_chart(&mut self, rel_id: &str) -> Result<ooxml_dml::types::ChartSpace> {
let rel = self
.doc_rels
.get(rel_id)
.ok_or_else(|| Error::MissingPart(format!("chart relationship {}", rel_id)))?;
let chart_path = resolve_path(&self.doc_path, &rel.target);
let chart_xml = self.package.read_part(&chart_path)?;
ext::parse_chart(&chart_xml).map_err(|e| Error::Invalid(e.to_string()))
}
pub fn save<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let file = File::create(path)?;
self.write(file)
}
pub fn write<W: std::io::Write + Seek>(&mut self, writer: W) -> Result<()> {
let doc_xml = serialize_xml(&self.gen_doc, "w:document")?;
let mut replacements = std::collections::HashMap::new();
replacements.insert(self.doc_path.as_str(), doc_xml.as_slice());
let styles_xml;
if let Some(ref styles_path) = self.styles_path {
styles_xml = serialize_xml(&self.gen_styles, "w:styles")?;
replacements.insert(styles_path.as_str(), styles_xml.as_slice());
}
let mut pkg_writer = PackageWriter::new(writer);
self.package
.copy_to_writer(&mut pkg_writer, &replacements)?;
pkg_writer.finish()?;
Ok(())
}
}
pub(crate) fn serialize_xml(value: &impl ToXml, tag: &str) -> Result<Vec<u8>> {
let inner = Vec::new();
let mut writer = quick_xml::Writer::new(inner);
value.write_element(tag, &mut writer)?;
let inner = writer.into_inner();
let mut buf = Vec::with_capacity(
b"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n".len() + inner.len(),
);
buf.extend_from_slice(b"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n");
buf.extend_from_slice(&inner);
Ok(buf)
}
pub(crate) fn resolve_path(base: &str, relative: &str) -> String {
if let Some(stripped) = relative.strip_prefix('/') {
return stripped.to_string();
}
if let Some(slash_pos) = base.rfind('/') {
format!("{}/{}", &base[..slash_pos], relative)
} else {
relative.to_string()
}
}
pub(crate) fn content_type_from_path(path: &str) -> String {
let ext = path.rsplit('.').next().unwrap_or("").to_lowercase();
match ext.as_str() {
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"gif" => "image/gif",
"bmp" => "image/bmp",
"tiff" | "tif" => "image/tiff",
"webp" => "image/webp",
"svg" => "image/svg+xml",
"emf" => "image/x-emf",
"wmf" => "image/x-wmf",
_ => "application/octet-stream",
}
.to_string()
}
#[derive(Debug, Clone)]
pub struct ImageData {
pub content_type: String,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Default)]
pub struct DocumentSettings {
pub default_tab_stop: Option<u32>,
pub zoom_percent: Option<u32>,
pub display_background_shape: bool,
pub track_revisions: bool,
pub do_not_track_moves: bool,
pub do_not_track_formatting: bool,
pub spelling_state: Option<ProofState>,
pub grammar_state: Option<ProofState>,
pub character_spacing_control: Option<CharacterSpacingControl>,
pub compat_mode: Option<u32>,
pub unknown_children: Vec<PositionedNode>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProofState {
Clean,
Dirty,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CharacterSpacingControl {
DoNotCompress,
CompressPunctuation,
CompressPunctuationAndJapaneseKana,
}
#[derive(Debug, Clone, Default)]
pub struct CoreProperties {
pub title: Option<String>,
pub creator: Option<String>,
pub subject: Option<String>,
pub description: Option<String>,
pub keywords: Option<String>,
pub category: Option<String>,
pub last_modified_by: Option<String>,
pub revision: Option<String>,
pub created: Option<String>,
pub modified: Option<String>,
pub content_status: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct AppProperties {
pub application: Option<String>,
pub app_version: Option<String>,
pub company: Option<String>,
pub manager: Option<String>,
pub total_time: Option<u32>,
pub pages: Option<u32>,
pub words: Option<u32>,
pub characters: Option<u32>,
pub characters_with_spaces: Option<u32>,
pub paragraphs: Option<u32>,
pub lines: Option<u32>,
pub template: Option<String>,
pub doc_security: Option<u32>,
}
const EL_SETTINGS: &[u8] = b"settings";
const EL_DEFAULT_TAB_STOP: &[u8] = b"defaultTabStop";
const EL_ZOOM: &[u8] = b"zoom";
const EL_DISPLAY_BACKGROUND_SHAPE: &[u8] = b"displayBackgroundShape";
const EL_TRACK_REVISIONS: &[u8] = b"trackRevisions";
const EL_DO_NOT_TRACK_MOVES: &[u8] = b"doNotTrackMoves";
const EL_DO_NOT_TRACK_FORMATTING: &[u8] = b"doNotTrackFormatting";
const EL_PROOF_STATE: &[u8] = b"proofState";
const EL_CHAR_SPACE_CONTROL: &[u8] = b"characterSpacingControl";
const EL_COMPAT: &[u8] = b"compat";
const EL_COMPAT_SETTING: &[u8] = b"compatSetting";
fn parse_settings(xml: &[u8]) -> Result<DocumentSettings> {
let mut reader = Reader::from_reader(xml);
reader.config_mut().trim_text(false);
let mut buf = Vec::new();
let mut settings = DocumentSettings::default();
let mut in_settings = false;
let mut in_compat = false;
let mut child_idx: usize = 0;
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == EL_SETTINGS {
in_settings = true;
} else if in_settings && local == EL_COMPAT {
in_compat = true;
child_idx += 1;
} else if in_settings && !in_compat {
let node = RawXmlElement::from_reader(&mut reader, &e)?;
settings
.unknown_children
.push(PositionedNode::new(child_idx, RawXmlNode::Element(node)));
child_idx += 1;
}
}
Ok(Event::Empty(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if !in_settings {
continue;
}
if local == EL_DEFAULT_TAB_STOP {
for attr in e.attributes().flatten() {
let key = local_name(attr.key.as_ref());
if key == b"val"
&& let Ok(s) = std::str::from_utf8(&attr.value)
{
settings.default_tab_stop = s.parse().ok();
}
}
child_idx += 1;
} else if local == EL_ZOOM {
for attr in e.attributes().flatten() {
let key = local_name(attr.key.as_ref());
if key == b"percent"
&& let Ok(s) = std::str::from_utf8(&attr.value)
{
settings.zoom_percent = s.parse().ok();
}
}
child_idx += 1;
} else if local == EL_DISPLAY_BACKGROUND_SHAPE {
settings.display_background_shape = parse_toggle_val(&e);
child_idx += 1;
} else if local == EL_TRACK_REVISIONS {
settings.track_revisions = parse_toggle_val(&e);
child_idx += 1;
} else if local == EL_DO_NOT_TRACK_MOVES {
settings.do_not_track_moves = parse_toggle_val(&e);
child_idx += 1;
} else if local == EL_DO_NOT_TRACK_FORMATTING {
settings.do_not_track_formatting = parse_toggle_val(&e);
child_idx += 1;
} else if local == EL_PROOF_STATE {
for attr in e.attributes().flatten() {
let key = local_name(attr.key.as_ref());
if let Ok(s) = std::str::from_utf8(&attr.value) {
if key == b"spelling" {
settings.spelling_state = match s {
"clean" => Some(ProofState::Clean),
"dirty" => Some(ProofState::Dirty),
_ => None,
};
} else if key == b"grammar" {
settings.grammar_state = match s {
"clean" => Some(ProofState::Clean),
"dirty" => Some(ProofState::Dirty),
_ => None,
};
}
}
}
child_idx += 1;
} else if local == EL_CHAR_SPACE_CONTROL {
for attr in e.attributes().flatten() {
let key = local_name(attr.key.as_ref());
if key == b"val"
&& let Ok(s) = std::str::from_utf8(&attr.value)
{
settings.character_spacing_control = match s {
"doNotCompress" => Some(CharacterSpacingControl::DoNotCompress),
"compressPunctuation" => {
Some(CharacterSpacingControl::CompressPunctuation)
}
"compressPunctuationAndJapaneseKana" => Some(
CharacterSpacingControl::CompressPunctuationAndJapaneseKana,
),
_ => None,
};
}
}
child_idx += 1;
} else if in_compat && local == EL_COMPAT_SETTING {
for attr in e.attributes().flatten() {
let key = local_name(attr.key.as_ref());
if key == b"name" && &*attr.value == b"compatibilityMode" {
for attr2 in e.attributes().flatten() {
let key2 = local_name(attr2.key.as_ref());
if key2 == b"val"
&& let Ok(s) = std::str::from_utf8(&attr2.value)
{
settings.compat_mode = s.parse().ok();
}
}
}
}
} else if !in_compat {
let node = RawXmlElement::from_empty(&e);
settings
.unknown_children
.push(PositionedNode::new(child_idx, RawXmlNode::Element(node)));
child_idx += 1;
}
}
Ok(Event::End(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == EL_SETTINGS {
break;
} else if local == EL_COMPAT {
in_compat = false;
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(Error::Xml(e)
.with_context("word/settings.xml")
.at_position(reader.error_position()));
}
_ => {}
}
buf.clear();
}
Ok(settings)
}
fn parse_core_properties(xml: &[u8]) -> Result<CoreProperties> {
let mut reader = Reader::from_reader(xml);
reader.config_mut().trim_text(false);
let mut buf = Vec::new();
let mut props = CoreProperties::default();
let mut in_core = false;
let mut current_element: Option<&'static str> = None;
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"coreProperties" {
in_core = true;
} else if in_core {
current_element = match local {
b"title" => Some("title"),
b"creator" => Some("creator"),
b"subject" => Some("subject"),
b"description" => Some("description"),
b"keywords" => Some("keywords"),
b"category" => Some("category"),
b"lastModifiedBy" => Some("lastModifiedBy"),
b"revision" => Some("revision"),
b"created" => Some("created"),
b"modified" => Some("modified"),
b"contentStatus" => Some("contentStatus"),
_ => None,
};
}
}
Ok(Event::End(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"coreProperties" {
in_core = false;
} else if in_core {
current_element = None;
}
}
Ok(Event::Text(e)) if current_element.is_some() => {
let text = e.decode().ok().map(|s| s.into_owned());
match current_element {
Some("title") => props.title = text,
Some("creator") => props.creator = text,
Some("subject") => props.subject = text,
Some("description") => props.description = text,
Some("keywords") => props.keywords = text,
Some("category") => props.category = text,
Some("lastModifiedBy") => props.last_modified_by = text,
Some("revision") => props.revision = text,
Some("created") => props.created = text,
Some("modified") => props.modified = text,
Some("contentStatus") => props.content_status = text,
_ => {}
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(Error::Xml(e)
.with_context("docProps/core.xml")
.at_position(reader.error_position()));
}
_ => {}
}
buf.clear();
}
Ok(props)
}
fn parse_app_properties(xml: &[u8]) -> Result<AppProperties> {
let mut reader = Reader::from_reader(xml);
reader.config_mut().trim_text(false);
let mut buf = Vec::new();
let mut props = AppProperties::default();
let mut in_props = false;
let mut current_element: Option<&'static str> = None;
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"Properties" {
in_props = true;
} else if in_props {
current_element = match local {
b"Application" => Some("Application"),
b"AppVersion" => Some("AppVersion"),
b"Company" => Some("Company"),
b"Manager" => Some("Manager"),
b"TotalTime" => Some("TotalTime"),
b"Pages" => Some("Pages"),
b"Words" => Some("Words"),
b"Characters" => Some("Characters"),
b"CharactersWithSpaces" => Some("CharactersWithSpaces"),
b"Paragraphs" => Some("Paragraphs"),
b"Lines" => Some("Lines"),
b"Template" => Some("Template"),
b"DocSecurity" => Some("DocSecurity"),
_ => None,
};
}
}
Ok(Event::End(e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"Properties" {
in_props = false;
} else if in_props {
current_element = None;
}
}
Ok(Event::Text(e)) if current_element.is_some() => {
let text = e.decode().ok().map(|s| s.into_owned());
match current_element {
Some("Application") => props.application = text,
Some("AppVersion") => props.app_version = text,
Some("Company") => props.company = text,
Some("Manager") => props.manager = text,
Some("TotalTime") => {
props.total_time = text.as_deref().and_then(|s| s.parse().ok())
}
Some("Pages") => props.pages = text.as_deref().and_then(|s| s.parse().ok()),
Some("Words") => props.words = text.as_deref().and_then(|s| s.parse().ok()),
Some("Characters") => {
props.characters = text.as_deref().and_then(|s| s.parse().ok())
}
Some("CharactersWithSpaces") => {
props.characters_with_spaces = text.as_deref().and_then(|s| s.parse().ok())
}
Some("Paragraphs") => {
props.paragraphs = text.as_deref().and_then(|s| s.parse().ok())
}
Some("Lines") => props.lines = text.as_deref().and_then(|s| s.parse().ok()),
Some("Template") => props.template = text,
Some("DocSecurity") => {
props.doc_security = text.as_deref().and_then(|s| s.parse().ok())
}
_ => {}
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(Error::Xml(e)
.with_context("docProps/app.xml")
.at_position(reader.error_position()));
}
_ => {}
}
buf.clear();
}
Ok(props)
}
fn local_name(name: &[u8]) -> &[u8] {
if let Some(pos) = name.iter().position(|&b| b == b':') {
&name[pos + 1..]
} else {
name
}
}
fn parse_toggle_val(e: &quick_xml::events::BytesStart) -> bool {
for attr in e.attributes().filter_map(|a| a.ok()) {
if attr.key.as_ref() == b"w:val" || attr.key.as_ref() == b"val" {
let val: &[u8] = &attr.value;
return matches!(val, b"true" | b"1" | b"on" | b"True" | b"On");
}
}
true
}
use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event as XmlEvent};
fn write_text_elem(writer: &mut quick_xml::Writer<Vec<u8>>, tag: &str, text: &str) -> Result<()> {
writer.write_event(XmlEvent::Start(BytesStart::new(tag)))?;
writer.write_event(XmlEvent::Text(BytesText::new(text)))?;
writer.write_event(XmlEvent::End(BytesEnd::new(tag)))?;
Ok(())
}
fn write_text_elem_attr(
writer: &mut quick_xml::Writer<Vec<u8>>,
tag: &str,
attr_name: &str,
attr_val: &str,
text: &str,
) -> Result<()> {
let mut start = BytesStart::new(tag);
start.push_attribute((attr_name, attr_val));
writer.write_event(XmlEvent::Start(start))?;
writer.write_event(XmlEvent::Text(BytesText::new(text)))?;
writer.write_event(XmlEvent::End(BytesEnd::new(tag)))?;
Ok(())
}
pub(crate) fn serialize_core_properties(props: &CoreProperties) -> Result<Vec<u8>> {
let mut writer = quick_xml::Writer::new(Vec::new());
writer.write_event(XmlEvent::Decl(BytesDecl::new(
"1.0",
Some("UTF-8"),
Some("yes"),
)))?;
let mut root = BytesStart::new("cp:coreProperties");
root.push_attribute((
"xmlns:cp",
"http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
));
root.push_attribute(("xmlns:dc", "http://purl.org/dc/elements/1.1/"));
root.push_attribute(("xmlns:dcterms", "http://purl.org/dc/terms/"));
root.push_attribute(("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"));
writer.write_event(XmlEvent::Start(root))?;
if let Some(ref v) = props.title {
write_text_elem(&mut writer, "dc:title", v)?;
}
if let Some(ref v) = props.creator {
write_text_elem(&mut writer, "dc:creator", v)?;
}
if let Some(ref v) = props.subject {
write_text_elem(&mut writer, "dc:subject", v)?;
}
if let Some(ref v) = props.description {
write_text_elem(&mut writer, "dc:description", v)?;
}
if let Some(ref v) = props.keywords {
write_text_elem(&mut writer, "cp:keywords", v)?;
}
if let Some(ref v) = props.category {
write_text_elem(&mut writer, "cp:category", v)?;
}
if let Some(ref v) = props.last_modified_by {
write_text_elem(&mut writer, "cp:lastModifiedBy", v)?;
}
if let Some(ref v) = props.revision {
write_text_elem(&mut writer, "cp:revision", v)?;
}
if let Some(ref v) = props.created {
write_text_elem_attr(
&mut writer,
"dcterms:created",
"xsi:type",
"dcterms:W3CDTF",
v,
)?;
}
if let Some(ref v) = props.modified {
write_text_elem_attr(
&mut writer,
"dcterms:modified",
"xsi:type",
"dcterms:W3CDTF",
v,
)?;
}
if let Some(ref v) = props.content_status {
write_text_elem(&mut writer, "cp:contentStatus", v)?;
}
writer.write_event(XmlEvent::End(BytesEnd::new("cp:coreProperties")))?;
Ok(writer.into_inner())
}
pub(crate) fn serialize_app_properties(props: &AppProperties) -> Result<Vec<u8>> {
let mut writer = quick_xml::Writer::new(Vec::new());
writer.write_event(XmlEvent::Decl(BytesDecl::new(
"1.0",
Some("UTF-8"),
Some("yes"),
)))?;
let mut root = BytesStart::new("Properties");
root.push_attribute((
"xmlns",
"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties",
));
root.push_attribute((
"xmlns:vt",
"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes",
));
writer.write_event(XmlEvent::Start(root))?;
if let Some(ref v) = props.application {
write_text_elem(&mut writer, "Application", v)?;
}
if let Some(ref v) = props.app_version {
write_text_elem(&mut writer, "AppVersion", v)?;
}
if let Some(ref v) = props.company {
write_text_elem(&mut writer, "Company", v)?;
}
if let Some(ref v) = props.manager {
write_text_elem(&mut writer, "Manager", v)?;
}
if let Some(v) = props.total_time {
write_text_elem(&mut writer, "TotalTime", &v.to_string())?;
}
if let Some(v) = props.pages {
write_text_elem(&mut writer, "Pages", &v.to_string())?;
}
if let Some(v) = props.words {
write_text_elem(&mut writer, "Words", &v.to_string())?;
}
if let Some(v) = props.characters {
write_text_elem(&mut writer, "Characters", &v.to_string())?;
}
if let Some(v) = props.characters_with_spaces {
write_text_elem(&mut writer, "CharactersWithSpaces", &v.to_string())?;
}
if let Some(v) = props.paragraphs {
write_text_elem(&mut writer, "Paragraphs", &v.to_string())?;
}
if let Some(v) = props.lines {
write_text_elem(&mut writer, "Lines", &v.to_string())?;
}
if let Some(ref v) = props.template {
write_text_elem(&mut writer, "Template", v)?;
}
if let Some(v) = props.doc_security {
write_text_elem(&mut writer, "DocSecurity", &v.to_string())?;
}
writer.write_event(XmlEvent::End(BytesEnd::new("Properties")))?;
Ok(writer.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_path() {
assert_eq!(
resolve_path("word/document.xml", "media/image1.png"),
"word/media/image1.png"
);
assert_eq!(
resolve_path("word/document.xml", "../media/image1.png"),
"word/../media/image1.png"
);
assert_eq!(
resolve_path("word/document.xml", "/word/media/image1.png"),
"word/media/image1.png"
);
}
#[test]
fn test_content_type_from_path() {
assert_eq!(content_type_from_path("word/media/image1.png"), "image/png");
assert_eq!(
content_type_from_path("word/media/image2.jpg"),
"image/jpeg"
);
assert_eq!(
content_type_from_path("word/media/image3.JPEG"),
"image/jpeg"
);
assert_eq!(content_type_from_path("word/media/image4.gif"), "image/gif");
assert_eq!(
content_type_from_path("word/media/unknown.xyz"),
"application/octet-stream"
);
}
#[test]
fn test_parse_core_properties() {
let xml = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:dcterms="http://purl.org/dc/terms/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dc:title>Test Document Title</dc:title>
<dc:creator>John Doe</dc:creator>
<dc:subject>Testing</dc:subject>
<dc:description>A test document for unit testing.</dc:description>
<cp:keywords>test, unit, document</cp:keywords>
<cp:category>Testing</cp:category>
<cp:lastModifiedBy>Jane Doe</cp:lastModifiedBy>
<cp:revision>5</cp:revision>
<dcterms:created xsi:type="dcterms:W3CDTF">2024-01-15T10:30:00Z</dcterms:created>
<dcterms:modified xsi:type="dcterms:W3CDTF">2024-01-16T14:45:00Z</dcterms:modified>
<cp:contentStatus>Draft</cp:contentStatus>
</cp:coreProperties>"#;
let props = parse_core_properties(xml).unwrap();
assert_eq!(props.title, Some("Test Document Title".to_string()));
assert_eq!(props.creator, Some("John Doe".to_string()));
assert_eq!(props.subject, Some("Testing".to_string()));
assert_eq!(
props.description,
Some("A test document for unit testing.".to_string())
);
assert_eq!(props.keywords, Some("test, unit, document".to_string()));
assert_eq!(props.category, Some("Testing".to_string()));
assert_eq!(props.last_modified_by, Some("Jane Doe".to_string()));
assert_eq!(props.revision, Some("5".to_string()));
assert_eq!(props.created, Some("2024-01-15T10:30:00Z".to_string()));
assert_eq!(props.modified, Some("2024-01-16T14:45:00Z".to_string()));
assert_eq!(props.content_status, Some("Draft".to_string()));
}
#[test]
fn test_serialize_core_properties() {
let props = CoreProperties {
title: Some("My Doc".to_string()),
creator: Some("Alice".to_string()),
created: Some("2024-01-01T00:00:00Z".to_string()),
modified: Some("2024-01-02T00:00:00Z".to_string()),
..Default::default()
};
let bytes = serialize_core_properties(&props).unwrap();
let xml = String::from_utf8(bytes).unwrap();
assert!(xml.contains("<dc:title>My Doc</dc:title>"));
assert!(xml.contains("<dc:creator>Alice</dc:creator>"));
assert!(xml.contains(
r#"<dcterms:created xsi:type="dcterms:W3CDTF">2024-01-01T00:00:00Z</dcterms:created>"#
));
assert!(xml.contains("cp:coreProperties"));
let parsed = parse_core_properties(xml.as_bytes()).unwrap();
assert_eq!(parsed.title, Some("My Doc".to_string()));
assert_eq!(parsed.creator, Some("Alice".to_string()));
assert_eq!(parsed.created, Some("2024-01-01T00:00:00Z".to_string()));
}
#[test]
fn test_serialize_app_properties() {
let props = AppProperties {
application: Some("ooxml-wml".to_string()),
pages: Some(3),
words: Some(500),
..Default::default()
};
let bytes = serialize_app_properties(&props).unwrap();
let xml = String::from_utf8(bytes).unwrap();
assert!(xml.contains("<Application>ooxml-wml</Application>"));
assert!(xml.contains("<Pages>3</Pages>"));
assert!(xml.contains("<Words>500</Words>"));
let parsed = parse_app_properties(xml.as_bytes()).unwrap();
assert_eq!(parsed.application, Some("ooxml-wml".to_string()));
assert_eq!(parsed.pages, Some(3));
assert_eq!(parsed.words, Some(500));
}
#[test]
fn test_serialize_core_properties_xml_escape() {
let props = CoreProperties {
title: Some("A & B < C".to_string()),
..Default::default()
};
let bytes = serialize_core_properties(&props).unwrap();
let xml = String::from_utf8(bytes).unwrap();
assert!(xml.contains("<dc:title>A & B < C</dc:title>"));
}
#[test]
fn test_parse_app_properties() {
let xml = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
<Application>Microsoft Office Word</Application>
<AppVersion>16.0000</AppVersion>
<Company>Test Corp</Company>
<Manager>Project Lead</Manager>
<TotalTime>120</TotalTime>
<Pages>5</Pages>
<Words>1234</Words>
<Characters>6789</Characters>
<CharactersWithSpaces>8000</CharactersWithSpaces>
<Paragraphs>45</Paragraphs>
<Lines>100</Lines>
<Template>Normal.dotm</Template>
<DocSecurity>0</DocSecurity>
</Properties>"#;
let props = parse_app_properties(xml).unwrap();
assert_eq!(props.application, Some("Microsoft Office Word".to_string()));
assert_eq!(props.app_version, Some("16.0000".to_string()));
assert_eq!(props.company, Some("Test Corp".to_string()));
assert_eq!(props.manager, Some("Project Lead".to_string()));
assert_eq!(props.total_time, Some(120));
assert_eq!(props.pages, Some(5));
assert_eq!(props.words, Some(1234));
assert_eq!(props.characters, Some(6789));
assert_eq!(props.characters_with_spaces, Some(8000));
assert_eq!(props.paragraphs, Some(45));
assert_eq!(props.lines, Some(100));
assert_eq!(props.template, Some("Normal.dotm".to_string()));
assert_eq!(props.doc_security, Some(0));
}
}