1use quick_xml::Reader;
4use quick_xml::events::Event;
5
6use crate::error::Result;
7use crate::xml::local_name;
8
9#[derive(Debug, Clone, Default, PartialEq)]
11pub struct CoreProperties {
12 pub title: Option<String>,
14 pub creator: Option<String>,
16 pub subject: Option<String>,
18 pub description: Option<String>,
20 pub keywords: Option<String>,
22 pub last_modified_by: Option<String>,
24 pub created: Option<String>,
26 pub modified: Option<String>,
28 pub category: Option<String>,
30 pub content_status: Option<String>,
32 pub identifier: Option<String>,
34 pub language: Option<String>,
36 pub last_printed: Option<String>,
38 pub revision: Option<String>,
40 pub version: Option<String>,
42}
43
44impl CoreProperties {
45 pub fn from_xml(xml: &[u8]) -> Result<Self> {
47 let mut reader = Reader::from_reader(xml);
48 reader.config_mut().trim_text(true);
49
50 let mut props = CoreProperties::default();
51 let mut buf = Vec::new();
52
53 loop {
54 match reader.read_event_into(&mut buf) {
55 Ok(Event::Start(ref e)) => {
56 let name = e.name();
57 let field = match local_name(name.as_ref()) {
58 b"title" => &mut props.title,
59 b"creator" => &mut props.creator,
60 b"subject" => &mut props.subject,
61 b"description" => &mut props.description,
62 b"keywords" => &mut props.keywords,
63 b"lastModifiedBy" => &mut props.last_modified_by,
64 b"created" => &mut props.created,
65 b"modified" => &mut props.modified,
66 b"category" => &mut props.category,
67 b"contentStatus" => &mut props.content_status,
68 b"identifier" => &mut props.identifier,
69 b"language" => &mut props.language,
70 b"lastPrinted" => &mut props.last_printed,
71 b"revision" => &mut props.revision,
72 b"version" => &mut props.version,
73 _ => {
74 buf.clear();
75 continue;
76 }
77 };
78 let text = crate::xml_text::read_element_text(&mut reader, name);
82 if !text.is_empty() {
83 *field = Some(text);
84 }
85 }
86 Ok(Event::Eof) => break,
87 Err(e) => return Err(e.into()),
88 _ => {}
89 }
90 buf.clear();
91 }
92
93 Ok(props)
94 }
95
96 pub fn to_xml(&self) -> Result<Vec<u8>> {
98 use quick_xml::Writer;
99 use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText};
100
101 let mut writer = Writer::new_with_indent(Vec::new(), b' ', 2);
102
103 writer.write_event(Event::Decl(BytesDecl::new(
104 "1.0",
105 Some("UTF-8"),
106 Some("yes"),
107 )))?;
108
109 let mut root = BytesStart::new("cp:coreProperties");
110 root.push_attribute((
111 "xmlns:cp",
112 "http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
113 ));
114 root.push_attribute(("xmlns:dc", "http://purl.org/dc/elements/1.1/"));
115 root.push_attribute(("xmlns:dcterms", "http://purl.org/dc/terms/"));
116 root.push_attribute(("xmlns:dcmitype", "http://purl.org/dc/dcmitype/"));
117 root.push_attribute(("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"));
118 writer.write_event(Event::Start(root))?;
119
120 fn write_element<W: std::io::Write>(
121 writer: &mut Writer<W>,
122 tag: &str,
123 value: &Option<String>,
124 ) -> Result<()> {
125 if let Some(val) = value {
126 writer.write_event(Event::Start(BytesStart::new(tag)))?;
127 writer.write_event(Event::Text(BytesText::new(val)))?;
128 writer.write_event(Event::End(BytesEnd::new(tag)))?;
129 }
130 Ok(())
131 }
132
133 fn write_date_element<W: std::io::Write>(
134 writer: &mut Writer<W>,
135 tag: &str,
136 value: &Option<String>,
137 ) -> Result<()> {
138 if let Some(val) = value {
139 let mut e = BytesStart::new(tag);
140 e.push_attribute(("xsi:type", "dcterms:W3CDTF"));
141 writer.write_event(Event::Start(e))?;
142 writer.write_event(Event::Text(BytesText::new(val)))?;
143 writer.write_event(Event::End(BytesEnd::new(tag)))?;
144 }
145 Ok(())
146 }
147
148 write_element(&mut writer, "dc:title", &self.title)?;
149 write_element(&mut writer, "dc:subject", &self.subject)?;
150 write_element(&mut writer, "dc:creator", &self.creator)?;
151 write_element(&mut writer, "cp:keywords", &self.keywords)?;
152 write_element(&mut writer, "dc:description", &self.description)?;
153 write_element(&mut writer, "cp:lastModifiedBy", &self.last_modified_by)?;
154 write_element(&mut writer, "cp:revision", &self.revision)?;
155 write_element(&mut writer, "cp:lastPrinted", &self.last_printed)?;
156 write_date_element(&mut writer, "dcterms:created", &self.created)?;
157 write_date_element(&mut writer, "dcterms:modified", &self.modified)?;
158 write_element(&mut writer, "cp:category", &self.category)?;
159 write_element(&mut writer, "cp:contentStatus", &self.content_status)?;
160 write_element(&mut writer, "dc:identifier", &self.identifier)?;
161 write_element(&mut writer, "dc:language", &self.language)?;
162 write_element(&mut writer, "cp:version", &self.version)?;
163
164 writer.write_event(Event::End(BytesEnd::new("cp:coreProperties")))?;
165
166 Ok(writer.into_inner())
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn parse_core_properties() {
176 let xml = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
177<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
178 xmlns:dc="http://purl.org/dc/elements/1.1/"
179 xmlns:dcterms="http://purl.org/dc/terms/"
180 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
181 <dc:title>Test Document</dc:title>
182 <dc:creator>John Doe</dc:creator>
183 <dc:subject>Testing</dc:subject>
184 <dc:description>A test document</dc:description>
185 <cp:keywords>test, document</cp:keywords>
186 <cp:lastModifiedBy>Jane Doe</cp:lastModifiedBy>
187 <dcterms:created xsi:type="dcterms:W3CDTF">2024-01-15T10:30:00Z</dcterms:created>
188 <dcterms:modified xsi:type="dcterms:W3CDTF">2024-06-20T14:00:00Z</dcterms:modified>
189 <cp:category>Reports</cp:category>
190 <cp:contentStatus>Draft</cp:contentStatus>
191 <dc:identifier>DOC-7</dc:identifier>
192 <dc:language>en-GB</dc:language>
193 <cp:lastPrinted>2024-06-21T09:00:00Z</cp:lastPrinted>
194 <cp:revision>12</cp:revision>
195 <cp:version>1.4</cp:version>
196</cp:coreProperties>"#;
197
198 let props = CoreProperties::from_xml(xml).unwrap();
199 assert_eq!(props.title, Some("Test Document".to_string()));
200 assert_eq!(props.creator, Some("John Doe".to_string()));
201 assert_eq!(props.subject, Some("Testing".to_string()));
202 assert_eq!(props.description, Some("A test document".to_string()));
203 assert_eq!(props.keywords, Some("test, document".to_string()));
204 assert_eq!(props.last_modified_by, Some("Jane Doe".to_string()));
205 assert_eq!(props.created, Some("2024-01-15T10:30:00Z".to_string()));
206 assert_eq!(props.modified, Some("2024-06-20T14:00:00Z".to_string()));
207 assert_eq!(props.category.as_deref(), Some("Reports"));
208 assert_eq!(props.content_status.as_deref(), Some("Draft"));
209 assert_eq!(props.identifier.as_deref(), Some("DOC-7"));
210 assert_eq!(props.language.as_deref(), Some("en-GB"));
211 assert_eq!(props.last_printed.as_deref(), Some("2024-06-21T09:00:00Z"));
212 assert_eq!(props.revision.as_deref(), Some("12"));
213 assert_eq!(props.version.as_deref(), Some("1.4"));
214 }
215
216 #[test]
217 fn round_trip_core_properties() {
218 let text = |value: &str| Some(value.to_owned());
219 let props = CoreProperties {
220 title: text("My Title"),
221 creator: text("Author"),
222 subject: None,
223 description: None,
224 keywords: text("rust, docx"),
225 last_modified_by: None,
226 created: text("2024-01-01T00:00:00Z"),
227 modified: text("2024-06-01T00:00:00Z"),
228 category: text("Reports"),
229 content_status: text("Final"),
230 identifier: text("DOC-7"),
231 language: text("fr-FR"),
232 last_printed: text("2024-06-02T00:00:00Z"),
233 revision: text("3"),
234 version: text("2.0"),
235 };
236
237 let xml = props.to_xml().unwrap();
238 let parsed = CoreProperties::from_xml(&xml).unwrap();
239
240 assert_eq!(parsed, props);
241 let xml = String::from_utf8(xml).unwrap();
242 assert!(xml.contains("<cp:lastPrinted>2024-06-02T00:00:00Z</cp:lastPrinted>"));
243 assert!(xml.contains("<dc:language>fr-FR</dc:language>"));
244 }
245
246 #[test]
247 fn unset_added_properties_leave_the_serialized_part_unchanged() {
248 let props = CoreProperties {
249 title: Some("Title".to_owned()),
250 created: Some("2024-01-01T00:00:00Z".to_owned()),
251 ..CoreProperties::default()
252 };
253
254 let xml = String::from_utf8(props.to_xml().unwrap()).unwrap();
255
256 assert_eq!(
257 xml.lines().map(str::trim).collect::<String>(),
258 concat!(
259 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#,
260 r#"<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">"#,
261 "<dc:title>Title</dc:title>",
262 r#"<dcterms:created xsi:type="dcterms:W3CDTF">2024-01-01T00:00:00Z</dcterms:created>"#,
263 "</cp:coreProperties>",
264 )
265 );
266 }
267}