1use std::collections::HashMap;
8
9use chrono::{DateTime, NaiveDateTime};
10use quick_xml::de::from_reader as xml_from_reader;
11use quick_xml::se::to_string as xml_to_string;
12
13use crate::constants::{
14 CONTENT_TYPE_CUSTOM_PROPERTIES, DEFAULT_XML_PATH_DOC_PROPS_APP,
15 DEFAULT_XML_PATH_DOC_PROPS_CORE, DEFAULT_XML_PATH_DOC_PROPS_CUSTOM, DEFAULT_XML_PATH_RELS,
16 EXT_URI_CUSTOM_PROPERTY_FMT_ID, NAMESPACE_DOCUMENT_PROPERTIES_VARIANT_TYPES,
17 NAMESPACE_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE_METADATA_INITIATIVE, NAMESPACE_DUBLIN_CORE_TERMS,
18 NAMESPACE_XML_SCHEMA_INSTANCE, SOURCE_RELATIONSHIP_CUSTOM_PROPERTIES,
19};
20use crate::errors::{ErrParameterInvalid, Result};
21use crate::file::File;
22use crate::xml::app::{AppProperties, XlsxProperties};
23use crate::xml::content_types::{XlsxContentTypeEntry, XlsxOverride};
24use crate::xml::core::{DecodeCoreProperties, DocProperties, XlsxCoreProperties, XlsxDcTerms};
25use crate::xml::custom::{
26 CustomProperty, CustomPropertyValue, DecodeCustomProperties, DecodeProperty,
27 XlsxCustomProperties, XlsxProperty,
28};
29
30const NAMESPACE_CUSTOM_PROPERTIES: &str =
31 "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties";
32
33impl File {
34 pub fn set_app_props(&mut self, props: &AppProperties) -> Result<()> {
40 let mut app = self.doc_props_app_reader()?;
41 app.application = non_empty(props.application.clone());
42 app.scale_crop = Some(props.scale_crop);
43 app.doc_security = Some(props.doc_security as i64);
44 app.company = non_empty(props.company.clone());
45 app.links_up_to_date = Some(props.links_up_to_date);
46 app.hyperlinks_changed = Some(props.hyperlinks_changed);
47 app.app_version = non_empty(props.app_version.clone());
48 self.doc_props_app_writer(&app);
49 Ok(())
50 }
51
52 pub fn get_app_props(&self) -> Result<AppProperties> {
54 let app = self.doc_props_app_reader()?;
55 Ok(AppProperties {
56 application: app.application.unwrap_or_default(),
57 scale_crop: app.scale_crop.unwrap_or(false),
58 doc_security: app.doc_security.unwrap_or(0) as i32,
59 company: app.company.unwrap_or_default(),
60 links_up_to_date: app.links_up_to_date.unwrap_or(false),
61 hyperlinks_changed: app.hyperlinks_changed.unwrap_or(false),
62 app_version: app.app_version.unwrap_or_default(),
63 })
64 }
65
66 pub fn set_doc_props(&mut self, props: &DocProperties) -> Result<()> {
68 let core = self.doc_props_core_reader()?;
69 let mut new_props = XlsxCoreProperties {
70 dc: Some(NAMESPACE_DUBLIN_CORE.to_string()),
71 dcterms: Some(NAMESPACE_DUBLIN_CORE_TERMS.to_string()),
72 dcmitype: Some(NAMESPACE_DUBLIN_CORE_METADATA_INITIATIVE.to_string()),
73 xsi: Some(NAMESPACE_XML_SCHEMA_INSTANCE.to_string()),
74 title: core.title.clone(),
75 subject: core.subject.clone(),
76 creator: core.creator.clone(),
77 keywords: core.keywords.clone(),
78 description: core.description.clone(),
79 last_modified_by: core.last_modified_by.clone(),
80 language: core.language.clone(),
81 identifier: core.identifier.clone(),
82 revision: core.revision.clone(),
83 created: core.created.as_ref().map(|d| XlsxDcTerms {
84 text: d.text.clone(),
85 r#type: d.r#type.clone(),
86 }),
87 modified: core.modified.as_ref().map(|d| XlsxDcTerms {
88 text: d.text.clone(),
89 r#type: d.r#type.clone(),
90 }),
91 content_status: core.content_status.clone(),
92 category: core.category.clone(),
93 version: core.version.clone(),
94 };
95
96 if !props.category.is_empty() {
97 new_props.category = Some(props.category.clone());
98 } else {
99 new_props.category = None;
100 }
101 if !props.content_status.is_empty() {
102 new_props.content_status = Some(props.content_status.clone());
103 } else {
104 new_props.content_status = None;
105 }
106 if !props.creator.is_empty() {
107 new_props.creator = Some(props.creator.clone());
108 } else {
109 new_props.creator = None;
110 }
111 if !props.description.is_empty() {
112 new_props.description = Some(props.description.clone());
113 } else {
114 new_props.description = None;
115 }
116 if !props.identifier.is_empty() {
117 new_props.identifier = Some(props.identifier.clone());
118 } else {
119 new_props.identifier = None;
120 }
121 if !props.keywords.is_empty() {
122 new_props.keywords = Some(props.keywords.clone());
123 } else {
124 new_props.keywords = None;
125 }
126 if !props.last_modified_by.is_empty() {
127 new_props.last_modified_by = Some(props.last_modified_by.clone());
128 } else {
129 new_props.last_modified_by = None;
130 }
131 if !props.revision.is_empty() {
132 new_props.revision = Some(props.revision.clone());
133 } else {
134 new_props.revision = None;
135 }
136 if !props.subject.is_empty() {
137 new_props.subject = Some(props.subject.clone());
138 } else {
139 new_props.subject = None;
140 }
141 if !props.title.is_empty() {
142 new_props.title = Some(props.title.clone());
143 } else {
144 new_props.title = None;
145 }
146 if !props.language.is_empty() {
147 new_props.language = Some(props.language.clone());
148 } else {
149 new_props.language = None;
150 }
151 if !props.version.is_empty() {
152 new_props.version = Some(props.version.clone());
153 } else {
154 new_props.version = None;
155 }
156
157 if !props.created.is_empty() {
158 new_props.created = Some(XlsxDcTerms {
159 text: props.created.clone(),
160 r#type: Some("dcterms:W3CDTF".to_string()),
161 });
162 }
163 if !props.modified.is_empty() {
164 new_props.modified = Some(XlsxDcTerms {
165 text: props.modified.clone(),
166 r#type: Some("dcterms:W3CDTF".to_string()),
167 });
168 }
169
170 self.doc_props_core_writer(&new_props);
171 Ok(())
172 }
173
174 pub fn get_doc_props(&self) -> Result<DocProperties> {
176 let core = self.doc_props_core_reader()?;
177 let mut ret = DocProperties {
178 category: core.category.unwrap_or_default(),
179 content_status: core.content_status.unwrap_or_default(),
180 created: String::new(),
181 creator: core.creator.unwrap_or_default(),
182 description: core.description.unwrap_or_default(),
183 identifier: core.identifier.unwrap_or_default(),
184 keywords: core.keywords.unwrap_or_default(),
185 last_modified_by: core.last_modified_by.unwrap_or_default(),
186 modified: String::new(),
187 revision: core.revision.unwrap_or_default(),
188 subject: core.subject.unwrap_or_default(),
189 title: core.title.unwrap_or_default(),
190 language: core.language.unwrap_or_default(),
191 version: core.version.unwrap_or_default(),
192 };
193 if let Some(c) = core.created {
194 ret.created = c.text;
195 }
196 if let Some(m) = core.modified {
197 ret.modified = m.text;
198 }
199 Ok(ret)
200 }
201
202 pub fn set_custom_props(&mut self, prop: &CustomProperty) -> Result<()> {
205 if prop.name.is_empty() {
206 return Err(Box::new(ErrParameterInvalid));
207 }
208
209 let custom = self.custom_properties_reader()?;
210
211 let mut by_name: HashMap<String, XlsxProperty> = HashMap::new();
212 let mut max_pid: i64 = 1;
213 for p in &custom.property {
214 let name = p.name.clone().unwrap_or_default();
215 if !name.is_empty() {
216 max_pid = max_pid.max(p.pid);
217 by_name.insert(name, decode_property_to_xlsx(p));
218 }
219 }
220
221 match &prop.value {
222 None => {
223 by_name.remove(&prop.name);
224 }
225 Some(value) => {
226 let pid = if let Some(existing) = by_name.get(&prop.name) {
227 existing.pid
228 } else {
229 max_pid += 1;
230 max_pid
231 };
232 by_name.insert(
233 prop.name.clone(),
234 XlsxProperty {
235 fmt_id: EXT_URI_CUSTOM_PROPERTY_FMT_ID.to_string(),
236 pid,
237 name: Some(prop.name.clone()),
238 link_target: None,
239 ..custom_value_to_xlsx(value)?
240 },
241 );
242 }
243 }
244
245 self.ensure_custom_properties_relationship();
246 self.add_custom_properties_content_type()?;
247
248 let mut properties: Vec<XlsxProperty> = by_name.into_values().collect();
249 properties.sort_by_key(|p| p.pid);
250 self.custom_properties_writer(&XlsxCustomProperties {
251 xmlns: Some(NAMESPACE_CUSTOM_PROPERTIES.to_string()),
252 vt: Some(NAMESPACE_DOCUMENT_PROPERTIES_VARIANT_TYPES.to_string()),
253 property: properties,
254 });
255 Ok(())
256 }
257
258 pub fn get_custom_props(&self) -> Result<Vec<CustomProperty>> {
260 let custom = self.custom_properties_reader()?;
261 let mut props = Vec::new();
262 for p in &custom.property {
263 props.push(CustomProperty {
264 name: p.name.clone().unwrap_or_default(),
265 value: decode_property_value(p),
266 });
267 }
268 Ok(props)
269 }
270
271 pub fn delete_custom_props(&mut self, name: &str) -> Result<()> {
273 if name.is_empty() {
274 return Err(Box::new(ErrParameterInvalid));
275 }
276 self.set_custom_props(&CustomProperty {
277 name: name.to_string(),
278 value: None,
279 })
280 }
281
282 fn doc_props_app_reader(&self) -> Result<XlsxProperties> {
287 let data = crate::file::namespace_strict_to_transitional(
288 &self.read_xml(DEFAULT_XML_PATH_DOC_PROPS_APP),
289 );
290 if data.is_empty() {
291 return Ok(XlsxProperties::default());
292 }
293 if !self.xml_attr.contains_key(DEFAULT_XML_PATH_DOC_PROPS_APP) {
294 if let Some(attrs) = crate::file::extract_root_namespace_attributes(&data) {
295 self.xml_attr
296 .insert(DEFAULT_XML_PATH_DOC_PROPS_APP.to_string(), attrs);
297 }
298 }
299 Ok(xml_from_reader(data.as_slice())?)
300 }
301
302 fn doc_props_core_reader(&self) -> Result<DecodeCoreProperties> {
303 let data = crate::file::namespace_strict_to_transitional(
304 &self.read_xml(DEFAULT_XML_PATH_DOC_PROPS_CORE),
305 );
306 if data.is_empty() {
307 return Ok(DecodeCoreProperties::default());
308 }
309 if !self.xml_attr.contains_key(DEFAULT_XML_PATH_DOC_PROPS_CORE) {
310 if let Some(attrs) = crate::file::extract_root_namespace_attributes(&data) {
311 self.xml_attr
312 .insert(DEFAULT_XML_PATH_DOC_PROPS_CORE.to_string(), attrs);
313 }
314 }
315 Ok(xml_from_reader(data.as_slice())?)
316 }
317
318 fn custom_properties_reader(&self) -> Result<DecodeCustomProperties> {
319 let data = crate::file::namespace_strict_to_transitional(
320 &self.read_xml(DEFAULT_XML_PATH_DOC_PROPS_CUSTOM),
321 );
322 if data.is_empty() {
323 return Ok(DecodeCustomProperties::default());
324 }
325 Ok(xml_from_reader(data.as_slice())?)
326 }
327
328 fn doc_props_app_writer(&self, props: &XlsxProperties) {
329 if let Ok(mut output) = xml_to_string(props).map(|s| s.into_bytes()) {
330 self.replace_namespace_bytes_if_needed(DEFAULT_XML_PATH_DOC_PROPS_APP, &mut output);
331 self.save_file_list(DEFAULT_XML_PATH_DOC_PROPS_APP, &output);
332 }
333 }
334
335 fn doc_props_core_writer(&self, props: &XlsxCoreProperties) {
336 if let Ok(mut output) = xml_to_string(props).map(|s| s.into_bytes()) {
337 self.replace_namespace_bytes_if_needed(DEFAULT_XML_PATH_DOC_PROPS_CORE, &mut output);
338 if let Ok(s) = std::str::from_utf8(&output) {
341 let s = s
342 .replacen("<coreProperties", "<cp:coreProperties", 1)
343 .replacen("</coreProperties>", "</cp:coreProperties>", 1);
344 output = s.into_bytes();
345 }
346 self.save_file_list(DEFAULT_XML_PATH_DOC_PROPS_CORE, &output);
347 }
348 }
349
350 fn custom_properties_writer(&self, props: &XlsxCustomProperties) {
351 if let Ok(output) = xml_to_string(props).map(|s| s.into_bytes()) {
352 self.save_file_list(DEFAULT_XML_PATH_DOC_PROPS_CUSTOM, &output);
353 }
354 }
355
356 fn ensure_custom_properties_relationship(&self) {
361 if let Ok(Some(rels)) = self.rels_reader(DEFAULT_XML_PATH_RELS) {
362 if rels.relationships.iter().any(|r| {
363 r.r#type == SOURCE_RELATIONSHIP_CUSTOM_PROPERTIES
364 && r.target == DEFAULT_XML_PATH_DOC_PROPS_CUSTOM
365 }) {
366 return;
367 }
368 }
369 self.add_rels(
370 DEFAULT_XML_PATH_RELS,
371 SOURCE_RELATIONSHIP_CUSTOM_PROPERTIES,
372 DEFAULT_XML_PATH_DOC_PROPS_CUSTOM,
373 "",
374 );
375 }
376
377 fn add_custom_properties_content_type(&self) -> Result<()> {
378 let mut ct = self.content_types_reader()?;
379 let exists = ct.entries.iter().any(|e| match e {
380 XlsxContentTypeEntry::Override(o) => {
381 o.part_name == "/docProps/custom.xml"
382 && o.content_type == CONTENT_TYPE_CUSTOM_PROPERTIES
383 }
384 _ => false,
385 });
386 if !exists {
387 ct.entries
388 .push(XlsxContentTypeEntry::Override(XlsxOverride {
389 part_name: "/docProps/custom.xml".to_string(),
390 content_type: CONTENT_TYPE_CUSTOM_PROPERTIES.to_string(),
391 }));
392 *self.content_types.lock().unwrap() = Some(ct);
393 }
394 Ok(())
395 }
396}
397
398fn non_empty(s: String) -> Option<String> {
399 if s.is_empty() { None } else { Some(s) }
400}
401
402fn decode_property_to_xlsx(p: &DecodeProperty) -> XlsxProperty {
403 XlsxProperty {
404 fmt_id: p.fmt_id.clone(),
405 pid: p.pid,
406 name: p.name.clone(),
407 link_target: p.link_target.clone(),
408 vector: p.vector.clone(),
409 array: p.array.clone(),
410 blob: p.blob.clone(),
411 oblob: p.oblob.clone(),
412 empty: p.empty.clone(),
413 null: p.null.clone(),
414 i1: p.i1,
415 i2: p.i2,
416 i4: p.i4,
417 i8: p.i8,
418 int: p.int,
419 ui1: p.ui1,
420 ui2: p.ui2,
421 ui4: p.ui4,
422 ui8: p.ui8,
423 uint: p.uint,
424 r4: p.r4,
425 r8: p.r8,
426 decimal: p.decimal.clone(),
427 lpstr: p.lpstr.clone(),
428 lpwstr: p.lpwstr.clone(),
429 bstr: p.bstr.clone(),
430 date: p.date.clone(),
431 file_time: p.file_time.clone(),
432 r#bool: p.r#bool,
433 cy: p.cy.clone(),
434 error: p.error.clone(),
435 stream: p.stream.clone(),
436 ostream: p.ostream.clone(),
437 storage: p.storage.clone(),
438 ostorage: p.ostorage.clone(),
439 vstream: p.vstream.clone(),
440 cls_id: p.cls_id.clone(),
441 }
442}
443
444fn custom_value_to_xlsx(value: &CustomPropertyValue) -> Result<XlsxProperty> {
445 let mut p = XlsxProperty::default();
446 match value {
447 CustomPropertyValue::Int(v) => p.i4 = Some(*v),
448 CustomPropertyValue::Float(v) => p.r8 = Some(*v),
449 CustomPropertyValue::Bool(v) => p.r#bool = Some(*v),
450 CustomPropertyValue::String(v) => p.lpwstr = Some(v.clone()),
451 CustomPropertyValue::Date(v) => {
452 validate_date_string(v)?;
453 p.file_time = Some(v.clone());
454 }
455 }
456 Ok(p)
457}
458
459fn validate_date_string(s: &str) -> Result<()> {
460 if DateTime::parse_from_rfc3339(s).is_ok() {
461 return Ok(());
462 }
463 if NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f").is_ok() {
464 return Ok(());
465 }
466 Err(Box::new(ErrParameterInvalid))
467}
468
469fn decode_property_value(p: &DecodeProperty) -> Option<CustomPropertyValue> {
470 if let Some(v) = p.i1 {
471 return Some(CustomPropertyValue::Int(v as i32));
472 }
473 if let Some(v) = p.i2 {
474 return Some(CustomPropertyValue::Int(v as i32));
475 }
476 if let Some(v) = p.i4 {
477 return Some(CustomPropertyValue::Int(v));
478 }
479 if let Some(v) = p.i8 {
480 return Some(CustomPropertyValue::Int(v as i32));
481 }
482 if let Some(v) = p.int {
483 return Some(CustomPropertyValue::Int(v as i32));
484 }
485 if let Some(v) = p.ui1 {
486 return Some(CustomPropertyValue::Int(v as i32));
487 }
488 if let Some(v) = p.ui2 {
489 return Some(CustomPropertyValue::Int(v as i32));
490 }
491 if let Some(v) = p.ui4 {
492 return Some(CustomPropertyValue::Int(v as i32));
493 }
494 if let Some(v) = p.ui8 {
495 return Some(CustomPropertyValue::Int(v as i32));
496 }
497 if let Some(v) = p.uint {
498 return Some(CustomPropertyValue::Int(v as i32));
499 }
500 if let Some(v) = p.r4 {
501 return Some(CustomPropertyValue::Float(v as f64));
502 }
503 if let Some(v) = p.r8 {
504 return Some(CustomPropertyValue::Float(v));
505 }
506 if let Some(v) = p.r#bool {
507 return Some(CustomPropertyValue::Bool(v));
508 }
509 if let Some(v) = &p.lpwstr {
510 return Some(CustomPropertyValue::String(v.clone()));
511 }
512 if let Some(v) = &p.file_time {
513 return Some(CustomPropertyValue::Date(v.clone()));
514 }
515 if let Some(v) = &p.lpstr {
516 return Some(CustomPropertyValue::String(v.clone()));
517 }
518 None
519}