1use crate::mce::children;
7use crate::xml::{Event, Ns, Reader, Start, XmlError};
8
9#[derive(Clone, Debug, Default, PartialEq, Eq)]
11pub struct CoreProperties {
12 pub title: Option<String>,
13 pub subject: Option<String>,
14 pub creator: Option<String>,
15 pub keywords: Option<String>,
16 pub description: Option<String>,
17 pub last_modified_by: Option<String>,
18 pub revision: Option<String>,
19 pub created: Option<String>,
21 pub modified: Option<String>,
22 pub last_printed: Option<String>,
23 pub category: Option<String>,
24 pub content_status: Option<String>,
25 pub language: Option<String>,
26 pub identifier: Option<String>,
27 pub version: Option<String>,
28}
29
30impl CoreProperties {
31 pub fn parse(xml: &[u8]) -> Result<Self, XmlError> {
32 let mut reader = Reader::new(xml);
33 root(&mut reader, xml)?;
34 let mut props = Self::default();
35 children(&mut reader, &mut |reader, child| {
36 let slot = match (child.name.ns, child.name.local) {
37 (Ns::Dc, b"title") => &mut props.title,
38 (Ns::Dc, b"subject") => &mut props.subject,
39 (Ns::Dc, b"creator") => &mut props.creator,
40 (Ns::Dc, b"description") => &mut props.description,
41 (Ns::Dc, b"language") => &mut props.language,
42 (Ns::Dc, b"identifier") => &mut props.identifier,
43 (Ns::Cp, b"keywords") => &mut props.keywords,
44 (Ns::Cp, b"lastModifiedBy") => &mut props.last_modified_by,
45 (Ns::Cp, b"revision") => &mut props.revision,
46 (Ns::Cp, b"lastPrinted") => &mut props.last_printed,
47 (Ns::Cp, b"category") => &mut props.category,
48 (Ns::Cp, b"contentStatus") => &mut props.content_status,
49 (Ns::Cp, b"version") => &mut props.version,
50 (Ns::Dcterms, b"created") => &mut props.created,
51 (Ns::Dcterms, b"modified") => &mut props.modified,
52 _ => return reader.skip_element(),
53 };
54 *slot = text_of(reader)?;
55 Ok(())
56 })?;
57 Ok(props)
58 }
59
60 pub fn is_empty(&self) -> bool {
62 *self == Self::default()
63 }
64}
65
66#[derive(Clone, Debug, Default, PartialEq, Eq)]
68pub struct AppProperties {
69 pub application: Option<String>,
70 pub app_version: Option<String>,
71 pub company: Option<String>,
72 pub manager: Option<String>,
73 pub template: Option<String>,
74 pub presentation_format: Option<String>,
75 pub slides: Option<u64>,
76 pub notes: Option<u64>,
77 pub hidden_slides: Option<u64>,
78 pub words: Option<u64>,
79 pub paragraphs: Option<u64>,
80 pub multimedia_clips: Option<u64>,
81 pub total_time: Option<u64>,
83 pub titles_of_parts: Vec<String>,
85}
86
87impl AppProperties {
88 pub fn parse(xml: &[u8]) -> Result<Self, XmlError> {
89 let mut reader = Reader::new(xml);
90 root(&mut reader, xml)?;
91 let mut props = Self::default();
92 children(&mut reader, &mut |reader, child| {
93 if child.name.ns != Ns::Ep {
94 return reader.skip_element();
95 }
96 let text_slot = match child.name.local {
97 b"Application" => Some(&mut props.application),
98 b"AppVersion" => Some(&mut props.app_version),
99 b"Company" => Some(&mut props.company),
100 b"Manager" => Some(&mut props.manager),
101 b"Template" => Some(&mut props.template),
102 b"PresentationFormat" => Some(&mut props.presentation_format),
103 _ => None,
104 };
105 if let Some(slot) = text_slot {
106 *slot = text_of(reader)?;
107 return Ok(());
108 }
109 let count_slot = match child.name.local {
110 b"Slides" => Some(&mut props.slides),
111 b"Notes" => Some(&mut props.notes),
112 b"HiddenSlides" => Some(&mut props.hidden_slides),
113 b"Words" => Some(&mut props.words),
114 b"Paragraphs" => Some(&mut props.paragraphs),
115 b"MMClips" => Some(&mut props.multimedia_clips),
116 b"TotalTime" => Some(&mut props.total_time),
117 _ => None,
118 };
119 if let Some(slot) = count_slot {
120 *slot = text_of(reader)?.and_then(|text| text.parse().ok());
121 return Ok(());
122 }
123 if child.name.local == b"TitlesOfParts" {
124 return collect_strings(reader, &mut props.titles_of_parts);
125 }
126 reader.skip_element()
127 })?;
128 Ok(props)
129 }
130}
131
132fn root<'a>(reader: &mut Reader<'a>, xml: &[u8]) -> Result<Start<'a>, XmlError> {
134 loop {
135 match reader.next()? {
136 Event::Start(start) => return Ok(start),
137 Event::Eof => {
138 return Err(XmlError {
139 offset: xml.len(),
140 msg: "no root element",
141 })
142 }
143 _ => {}
144 }
145 }
146}
147
148fn text_of(reader: &mut Reader<'_>) -> Result<Option<String>, XmlError> {
150 let mut text = String::new();
151 reader.text_content(&mut text)?;
152 let trimmed = text.trim();
153 Ok(match trimmed.is_empty() {
154 true => None,
155 false => Some(trimmed.to_string()),
156 })
157}
158
159fn collect_strings(reader: &mut Reader<'_>, out: &mut Vec<String>) -> Result<(), XmlError> {
161 children(reader, &mut |reader, child| {
162 if child.name.is(Ns::Vt, b"vector") || child.name.is(Ns::Vt, b"variant") {
163 return collect_strings(reader, out);
164 }
165 if let Some(text) = text_of(reader)? {
166 out.push(text);
167 }
168 Ok(())
169 })
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn core_properties_read_dublin_core_and_opc_elements() {
178 let xml = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
179<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:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
180<dc:title>Quarterly & review</dc:title><dc:creator>Ada</dc:creator><cp:lastModifiedBy>Bob</cp:lastModifiedBy><cp:revision>7</cp:revision>
181<dcterms:created xsi:type="dcterms:W3CDTF">2024-05-01T10:00:00Z</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">2024-06-02T11:30:00Z</dcterms:modified>
182<dc:description></dc:description><cp:keywords> tags </cp:keywords><cp:unknown>x</cp:unknown></cp:coreProperties>"#;
183 let props = CoreProperties::parse(xml).unwrap();
184 assert_eq!(props.title.as_deref(), Some("Quarterly & review"));
185 assert_eq!(props.creator.as_deref(), Some("Ada"));
186 assert_eq!(props.last_modified_by.as_deref(), Some("Bob"));
187 assert_eq!(props.revision.as_deref(), Some("7"));
188 assert_eq!(props.created.as_deref(), Some("2024-05-01T10:00:00Z"));
189 assert_eq!(props.modified.as_deref(), Some("2024-06-02T11:30:00Z"));
190 assert_eq!(props.description, None);
191 assert_eq!(props.keywords.as_deref(), Some("tags"));
192 assert!(!props.is_empty());
193 assert!(CoreProperties::parse(br#"<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"/>"#).unwrap().is_empty());
194 }
195
196 #[test]
197 fn app_properties_read_counts_and_titles_of_parts() {
198 let xml = br#"<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
199<TotalTime>12</TotalTime><Words>345</Words><Application>Microsoft Macintosh PowerPoint</Application><PresentationFormat>Widescreen</PresentationFormat><Paragraphs>40</Paragraphs><Slides>7</Slides><Notes>2</Notes><HiddenSlides>0</HiddenSlides><MMClips>0</MMClips>
200<HeadingPairs><vt:vector size="4" baseType="variant"><vt:variant><vt:lpstr>Theme</vt:lpstr></vt:variant><vt:variant><vt:i4>1</vt:i4></vt:variant><vt:variant><vt:lpstr>Slide Titles</vt:lpstr></vt:variant><vt:variant><vt:i4>2</vt:i4></vt:variant></vt:vector></HeadingPairs>
201<TitlesOfParts><vt:vector size="3" baseType="lpstr"><vt:lpstr>Office Theme</vt:lpstr><vt:lpstr>Intro</vt:lpstr><vt:lpstr>Numbers</vt:lpstr></vt:vector></TitlesOfParts>
202<Company>ACME</Company><AppVersion>16.0000</AppVersion></Properties>"#;
203 let props = AppProperties::parse(xml).unwrap();
204 assert_eq!(props.total_time, Some(12));
205 assert_eq!(props.words, Some(345));
206 assert_eq!(props.slides, Some(7));
207 assert_eq!(props.notes, Some(2));
208 assert_eq!(props.hidden_slides, Some(0));
209 assert_eq!(
210 props.application.as_deref(),
211 Some("Microsoft Macintosh PowerPoint")
212 );
213 assert_eq!(props.presentation_format.as_deref(), Some("Widescreen"));
214 assert_eq!(props.company.as_deref(), Some("ACME"));
215 assert_eq!(props.app_version.as_deref(), Some("16.0000"));
216 assert_eq!(props.titles_of_parts, ["Office Theme", "Intro", "Numbers"]);
217 }
218}