1use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
9use quick_xml::Reader;
10use quick_xml::Writer;
11use serde::{Deserialize, Serialize};
12
13use crate::namespaces;
14
15#[derive(Debug, Clone, Default, PartialEq)]
21pub struct CoreProperties {
22 pub title: Option<String>,
23 pub subject: Option<String>,
24 pub creator: Option<String>,
25 pub keywords: Option<String>,
26 pub description: Option<String>,
27 pub last_modified_by: Option<String>,
28 pub revision: Option<String>,
29 pub created: Option<String>,
30 pub modified: Option<String>,
31 pub category: Option<String>,
32 pub content_status: Option<String>,
33}
34
35pub fn serialize_core_properties(props: &CoreProperties) -> String {
37 let mut writer = Writer::new(Vec::new());
38
39 writer
41 .write_event(Event::Decl(BytesDecl::new(
42 "1.0",
43 Some("UTF-8"),
44 Some("yes"),
45 )))
46 .unwrap();
47
48 let mut root = BytesStart::new("cp:coreProperties");
50 root.push_attribute(("xmlns:cp", namespaces::CORE_PROPERTIES));
51 root.push_attribute(("xmlns:dc", namespaces::DC));
52 root.push_attribute(("xmlns:dcterms", namespaces::DC_TERMS));
53 root.push_attribute(("xmlns:dcmitype", DC_MITYPE));
54 root.push_attribute(("xmlns:xsi", namespaces::XSI));
55 writer.write_event(Event::Start(root)).unwrap();
56
57 fn write_element(writer: &mut Writer<Vec<u8>>, tag: &str, value: &str) {
59 writer
60 .write_event(Event::Start(BytesStart::new(tag)))
61 .unwrap();
62 writer
63 .write_event(Event::Text(BytesText::new(value)))
64 .unwrap();
65 writer.write_event(Event::End(BytesEnd::new(tag))).unwrap();
66 }
67
68 fn write_dcterms_element(writer: &mut Writer<Vec<u8>>, tag: &str, value: &str) {
70 let mut start = BytesStart::new(tag);
71 start.push_attribute(("xsi:type", "dcterms:W3CDTF"));
72 writer.write_event(Event::Start(start)).unwrap();
73 writer
74 .write_event(Event::Text(BytesText::new(value)))
75 .unwrap();
76 writer.write_event(Event::End(BytesEnd::new(tag))).unwrap();
77 }
78
79 if let Some(ref v) = props.title {
80 write_element(&mut writer, "dc:title", v);
81 }
82 if let Some(ref v) = props.subject {
83 write_element(&mut writer, "dc:subject", v);
84 }
85 if let Some(ref v) = props.creator {
86 write_element(&mut writer, "dc:creator", v);
87 }
88 if let Some(ref v) = props.keywords {
89 write_element(&mut writer, "cp:keywords", v);
90 }
91 if let Some(ref v) = props.description {
92 write_element(&mut writer, "dc:description", v);
93 }
94 if let Some(ref v) = props.last_modified_by {
95 write_element(&mut writer, "cp:lastModifiedBy", v);
96 }
97 if let Some(ref v) = props.revision {
98 write_element(&mut writer, "cp:revision", v);
99 }
100 if let Some(ref v) = props.created {
101 write_dcterms_element(&mut writer, "dcterms:created", v);
102 }
103 if let Some(ref v) = props.modified {
104 write_dcterms_element(&mut writer, "dcterms:modified", v);
105 }
106 if let Some(ref v) = props.category {
107 write_element(&mut writer, "cp:category", v);
108 }
109 if let Some(ref v) = props.content_status {
110 write_element(&mut writer, "cp:contentStatus", v);
111 }
112
113 writer
114 .write_event(Event::End(BytesEnd::new("cp:coreProperties")))
115 .unwrap();
116
117 String::from_utf8(writer.into_inner()).unwrap()
118}
119
120pub fn deserialize_core_properties(xml: &str) -> Result<CoreProperties, String> {
122 let mut reader = Reader::from_str(xml);
123 reader.config_mut().trim_text(true);
124
125 let mut props = CoreProperties::default();
126 let mut current_tag: Option<String> = None;
127
128 loop {
129 match reader.read_event() {
130 Ok(Event::Start(ref e)) => {
131 let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
132 current_tag = Some(name);
133 }
134 Ok(Event::Text(ref e)) => {
135 if let Some(ref tag) = current_tag {
136 let text = e.unescape().unwrap_or_default().to_string();
137 match tag.as_str() {
138 "dc:title" | "title" => props.title = Some(text),
139 "dc:subject" | "subject" => props.subject = Some(text),
140 "dc:creator" | "creator" => props.creator = Some(text),
141 "cp:keywords" | "keywords" => props.keywords = Some(text),
142 "dc:description" | "description" => props.description = Some(text),
143 "cp:lastModifiedBy" | "lastModifiedBy" => {
144 props.last_modified_by = Some(text);
145 }
146 "cp:revision" | "revision" => props.revision = Some(text),
147 "dcterms:created" | "created" => props.created = Some(text),
148 "dcterms:modified" | "modified" => props.modified = Some(text),
149 "cp:category" | "category" => props.category = Some(text),
150 "cp:contentStatus" | "contentStatus" => {
151 props.content_status = Some(text);
152 }
153 _ => {}
154 }
155 }
156 }
157 Ok(Event::End(_)) => {
158 current_tag = None;
159 }
160 Ok(Event::Eof) => break,
161 Err(e) => return Err(format!("XML parse error: {e}")),
162 _ => {}
163 }
164 }
165
166 Ok(props)
167}
168
169const DC_MITYPE: &str = "http://purl.org/dc/dcmitype/";
171
172#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
174#[serde(rename = "Properties")]
175pub struct ExtendedProperties {
176 #[serde(rename = "@xmlns")]
177 pub xmlns: String,
178 #[serde(rename = "@xmlns:vt", skip_serializing_if = "Option::is_none")]
179 pub xmlns_vt: Option<String>,
180
181 #[serde(rename = "Application", skip_serializing_if = "Option::is_none")]
182 pub application: Option<String>,
183 #[serde(rename = "DocSecurity", skip_serializing_if = "Option::is_none")]
184 pub doc_security: Option<u32>,
185 #[serde(rename = "ScaleCrop", skip_serializing_if = "Option::is_none")]
186 pub scale_crop: Option<bool>,
187 #[serde(rename = "Company", skip_serializing_if = "Option::is_none")]
188 pub company: Option<String>,
189 #[serde(rename = "LinksUpToDate", skip_serializing_if = "Option::is_none")]
190 pub links_up_to_date: Option<bool>,
191 #[serde(rename = "SharedDoc", skip_serializing_if = "Option::is_none")]
192 pub shared_doc: Option<bool>,
193 #[serde(rename = "HyperlinksChanged", skip_serializing_if = "Option::is_none")]
194 pub hyperlinks_changed: Option<bool>,
195 #[serde(rename = "AppVersion", skip_serializing_if = "Option::is_none")]
196 pub app_version: Option<String>,
197 #[serde(rename = "Template", skip_serializing_if = "Option::is_none")]
198 pub template: Option<String>,
199 #[serde(rename = "Manager", skip_serializing_if = "Option::is_none")]
200 pub manager: Option<String>,
201}
202
203impl ExtendedProperties {
204 pub fn with_defaults() -> Self {
206 Self {
207 xmlns: namespaces::EXTENDED_PROPERTIES.to_string(),
208 xmlns_vt: Some(namespaces::VT.to_string()),
209 ..Default::default()
210 }
211 }
212}
213
214#[derive(Debug, Clone, Default, PartialEq)]
219pub struct CustomProperties {
220 pub properties: Vec<CustomProperty>,
221}
222
223#[derive(Debug, Clone, PartialEq)]
225pub struct CustomProperty {
226 pub fmtid: String,
227 pub pid: u32,
228 pub name: String,
229 pub value: CustomPropertyValue,
230}
231
232#[derive(Debug, Clone, PartialEq)]
234pub enum CustomPropertyValue {
235 String(String),
236 Int(i32),
237 Float(f64),
238 Bool(bool),
239 DateTime(String),
240}
241
242pub const CUSTOM_PROPERTY_FMTID: &str = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}";
244
245pub fn serialize_custom_properties(props: &CustomProperties) -> String {
247 let mut writer = Writer::new(Vec::new());
248
249 writer
251 .write_event(Event::Decl(BytesDecl::new(
252 "1.0",
253 Some("UTF-8"),
254 Some("yes"),
255 )))
256 .unwrap();
257
258 let mut root = BytesStart::new("Properties");
259 root.push_attribute(("xmlns", namespaces::CUSTOM_PROPERTIES));
260 root.push_attribute(("xmlns:vt", namespaces::VT));
261 writer.write_event(Event::Start(root)).unwrap();
262
263 for prop in &props.properties {
264 let mut elem = BytesStart::new("property");
265 elem.push_attribute(("fmtid", prop.fmtid.as_str()));
266 elem.push_attribute(("pid", prop.pid.to_string().as_str()));
267 elem.push_attribute(("name", prop.name.as_str()));
268 writer.write_event(Event::Start(elem)).unwrap();
269
270 match &prop.value {
271 CustomPropertyValue::String(s) => {
272 writer
273 .write_event(Event::Start(BytesStart::new("vt:lpwstr")))
274 .unwrap();
275 writer.write_event(Event::Text(BytesText::new(s))).unwrap();
276 writer
277 .write_event(Event::End(BytesEnd::new("vt:lpwstr")))
278 .unwrap();
279 }
280 CustomPropertyValue::Int(n) => {
281 writer
282 .write_event(Event::Start(BytesStart::new("vt:i4")))
283 .unwrap();
284 writer
285 .write_event(Event::Text(BytesText::new(&n.to_string())))
286 .unwrap();
287 writer
288 .write_event(Event::End(BytesEnd::new("vt:i4")))
289 .unwrap();
290 }
291 CustomPropertyValue::Float(f) => {
292 writer
293 .write_event(Event::Start(BytesStart::new("vt:r8")))
294 .unwrap();
295 writer
296 .write_event(Event::Text(BytesText::new(&f.to_string())))
297 .unwrap();
298 writer
299 .write_event(Event::End(BytesEnd::new("vt:r8")))
300 .unwrap();
301 }
302 CustomPropertyValue::Bool(b) => {
303 writer
304 .write_event(Event::Start(BytesStart::new("vt:bool")))
305 .unwrap();
306 writer
307 .write_event(Event::Text(BytesText::new(if *b {
308 "true"
309 } else {
310 "false"
311 })))
312 .unwrap();
313 writer
314 .write_event(Event::End(BytesEnd::new("vt:bool")))
315 .unwrap();
316 }
317 CustomPropertyValue::DateTime(dt) => {
318 writer
319 .write_event(Event::Start(BytesStart::new("vt:filetime")))
320 .unwrap();
321 writer.write_event(Event::Text(BytesText::new(dt))).unwrap();
322 writer
323 .write_event(Event::End(BytesEnd::new("vt:filetime")))
324 .unwrap();
325 }
326 }
327
328 writer
329 .write_event(Event::End(BytesEnd::new("property")))
330 .unwrap();
331 }
332
333 writer
334 .write_event(Event::End(BytesEnd::new("Properties")))
335 .unwrap();
336
337 String::from_utf8(writer.into_inner()).unwrap()
338}
339
340pub fn deserialize_custom_properties(xml: &str) -> Result<CustomProperties, String> {
342 let mut reader = Reader::from_str(xml);
343 reader.config_mut().trim_text(true);
344
345 let mut props = CustomProperties::default();
346
347 let mut current_fmtid: Option<String> = None;
349 let mut current_pid: Option<u32> = None;
350 let mut current_name: Option<String> = None;
351 let mut current_value_tag: Option<String> = None;
352
353 loop {
354 match reader.read_event() {
355 Ok(Event::Start(ref e)) => {
356 let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
357 if tag == "property" {
358 for attr in e.attributes().flatten() {
360 let key = String::from_utf8_lossy(attr.key.as_ref()).to_string();
361 let val = String::from_utf8_lossy(&attr.value).to_string();
362 match key.as_str() {
363 "fmtid" => current_fmtid = Some(val),
364 "pid" => current_pid = val.parse().ok(),
365 "name" => current_name = Some(val),
366 _ => {}
367 }
368 }
369 } else if tag.starts_with("vt:")
370 || matches!(tag.as_str(), "lpwstr" | "i4" | "r8" | "bool" | "filetime")
371 {
372 current_value_tag = Some(tag);
373 }
374 }
375 Ok(Event::Text(ref e)) => {
376 if let Some(ref vtag) = current_value_tag {
377 let text = e.unescape().unwrap_or_default().to_string();
378 let value = match vtag.as_str() {
379 "vt:lpwstr" | "lpwstr" => Some(CustomPropertyValue::String(text)),
380 "vt:i4" | "i4" => text.parse::<i32>().ok().map(CustomPropertyValue::Int),
381 "vt:r8" | "r8" => text.parse::<f64>().ok().map(CustomPropertyValue::Float),
382 "vt:bool" | "bool" => {
383 Some(CustomPropertyValue::Bool(text == "true" || text == "1"))
384 }
385 "vt:filetime" | "filetime" => Some(CustomPropertyValue::DateTime(text)),
386 _ => None,
387 };
388 if let (Some(fmtid), Some(pid), Some(name), Some(val)) = (
389 current_fmtid.take(),
390 current_pid.take(),
391 current_name.take(),
392 value,
393 ) {
394 props.properties.push(CustomProperty {
395 fmtid,
396 pid,
397 name,
398 value: val,
399 });
400 }
401 }
402 }
403 Ok(Event::End(ref e)) => {
404 let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
405 if tag.starts_with("vt:")
406 || matches!(tag.as_str(), "lpwstr" | "i4" | "r8" | "bool" | "filetime")
407 {
408 current_value_tag = None;
409 }
410 }
411 Ok(Event::Eof) => break,
412 Err(e) => return Err(format!("XML parse error: {e}")),
413 _ => {}
414 }
415 }
416
417 Ok(props)
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn test_core_properties_roundtrip() {
426 let props = CoreProperties {
427 title: Some("Test Title".to_string()),
428 subject: Some("Test Subject".to_string()),
429 creator: Some("Test Author".to_string()),
430 keywords: Some("key1, key2".to_string()),
431 description: Some("A description".to_string()),
432 last_modified_by: Some("Editor".to_string()),
433 revision: Some("3".to_string()),
434 created: Some("2024-01-01T00:00:00Z".to_string()),
435 modified: Some("2024-06-15T12:30:00Z".to_string()),
436 category: Some("Reports".to_string()),
437 content_status: Some("Draft".to_string()),
438 };
439
440 let xml = serialize_core_properties(&props);
441 let parsed = deserialize_core_properties(&xml).unwrap();
442 assert_eq!(props, parsed);
443 }
444
445 #[test]
446 fn test_core_properties_empty_fields() {
447 let props = CoreProperties::default();
448 let xml = serialize_core_properties(&props);
449 let parsed = deserialize_core_properties(&xml).unwrap();
450 assert_eq!(props, parsed);
451 }
452
453 #[test]
454 fn test_core_properties_partial_fields() {
455 let props = CoreProperties {
456 title: Some("Only Title".to_string()),
457 creator: Some("Only Author".to_string()),
458 ..Default::default()
459 };
460
461 let xml = serialize_core_properties(&props);
462 let parsed = deserialize_core_properties(&xml).unwrap();
463 assert_eq!(props, parsed);
464 }
465
466 #[test]
467 fn test_core_properties_serialized_format() {
468 let props = CoreProperties {
469 title: Some("My Title".to_string()),
470 creator: Some("Author Name".to_string()),
471 created: Some("2024-01-01T00:00:00Z".to_string()),
472 ..Default::default()
473 };
474
475 let xml = serialize_core_properties(&props);
476 assert!(xml.contains("<cp:coreProperties"));
477 assert!(xml.contains("xmlns:cp="));
478 assert!(xml.contains("xmlns:dc="));
479 assert!(xml.contains("xmlns:dcterms="));
480 assert!(xml.contains("<dc:title>My Title</dc:title>"));
481 assert!(xml.contains("<dc:creator>Author Name</dc:creator>"));
482 assert!(xml.contains("xsi:type=\"dcterms:W3CDTF\""));
483 assert!(xml.contains("<dcterms:created"));
484 assert!(xml.contains("2024-01-01T00:00:00Z</dcterms:created>"));
485 }
486
487 #[test]
488 fn test_parse_real_excel_core_xml() {
489 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
490<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">
491 <dc:title>Budget Report</dc:title>
492 <dc:subject>Finance</dc:subject>
493 <dc:creator>John Doe</dc:creator>
494 <cp:keywords>budget, 2024</cp:keywords>
495 <dc:description>Annual budget report</dc:description>
496 <cp:lastModifiedBy>Jane Smith</cp:lastModifiedBy>
497 <cp:revision>5</cp:revision>
498 <dcterms:created xsi:type="dcterms:W3CDTF">2024-01-15T08:00:00Z</dcterms:created>
499 <dcterms:modified xsi:type="dcterms:W3CDTF">2024-06-20T16:45:00Z</dcterms:modified>
500 <cp:category>Financial</cp:category>
501 <cp:contentStatus>Final</cp:contentStatus>
502</cp:coreProperties>"#;
503
504 let props = deserialize_core_properties(xml).unwrap();
505 assert_eq!(props.title.as_deref(), Some("Budget Report"));
506 assert_eq!(props.subject.as_deref(), Some("Finance"));
507 assert_eq!(props.creator.as_deref(), Some("John Doe"));
508 assert_eq!(props.keywords.as_deref(), Some("budget, 2024"));
509 assert_eq!(props.description.as_deref(), Some("Annual budget report"));
510 assert_eq!(props.last_modified_by.as_deref(), Some("Jane Smith"));
511 assert_eq!(props.revision.as_deref(), Some("5"));
512 assert_eq!(props.created.as_deref(), Some("2024-01-15T08:00:00Z"));
513 assert_eq!(props.modified.as_deref(), Some("2024-06-20T16:45:00Z"));
514 assert_eq!(props.category.as_deref(), Some("Financial"));
515 assert_eq!(props.content_status.as_deref(), Some("Final"));
516 }
517
518 #[test]
519 fn test_extended_properties_serde_roundtrip() {
520 let props = ExtendedProperties {
521 xmlns: namespaces::EXTENDED_PROPERTIES.to_string(),
522 xmlns_vt: Some(namespaces::VT.to_string()),
523 application: Some("SheetKit".to_string()),
524 doc_security: Some(0),
525 scale_crop: Some(false),
526 company: Some("Acme Corp".to_string()),
527 links_up_to_date: Some(false),
528 shared_doc: Some(false),
529 hyperlinks_changed: Some(false),
530 app_version: Some("1.0.0".to_string()),
531 template: None,
532 manager: Some("Boss".to_string()),
533 };
534
535 let xml = quick_xml::se::to_string(&props).unwrap();
536 let parsed: ExtendedProperties = quick_xml::de::from_str(&xml).unwrap();
537 assert_eq!(props, parsed);
538 }
539
540 #[test]
541 fn test_extended_properties_with_defaults() {
542 let props = ExtendedProperties::with_defaults();
543 assert_eq!(props.xmlns, namespaces::EXTENDED_PROPERTIES);
544 assert_eq!(props.xmlns_vt.as_deref(), Some(namespaces::VT));
545 assert!(props.application.is_none());
546 }
547
548 #[test]
549 fn test_extended_properties_skip_none_fields() {
550 let props = ExtendedProperties {
551 xmlns: namespaces::EXTENDED_PROPERTIES.to_string(),
552 xmlns_vt: None,
553 application: Some("Test".to_string()),
554 doc_security: None,
555 scale_crop: None,
556 company: None,
557 links_up_to_date: None,
558 shared_doc: None,
559 hyperlinks_changed: None,
560 app_version: None,
561 template: None,
562 manager: None,
563 };
564
565 let xml = quick_xml::se::to_string(&props).unwrap();
566 assert!(xml.contains("<Application>Test</Application>"));
567 assert!(!xml.contains("DocSecurity"));
568 assert!(!xml.contains("Company"));
569 }
570
571 #[test]
572 fn test_custom_properties_roundtrip() {
573 let props = CustomProperties {
574 properties: vec![
575 CustomProperty {
576 fmtid: CUSTOM_PROPERTY_FMTID.to_string(),
577 pid: 2,
578 name: "Project".to_string(),
579 value: CustomPropertyValue::String("SheetKit".to_string()),
580 },
581 CustomProperty {
582 fmtid: CUSTOM_PROPERTY_FMTID.to_string(),
583 pid: 3,
584 name: "Version".to_string(),
585 value: CustomPropertyValue::Int(42),
586 },
587 ],
588 };
589
590 let xml = serialize_custom_properties(&props);
591 let parsed = deserialize_custom_properties(&xml).unwrap();
592 assert_eq!(props, parsed);
593 }
594
595 #[test]
596 fn test_custom_properties_all_value_types() {
597 let props = CustomProperties {
598 properties: vec![
599 CustomProperty {
600 fmtid: CUSTOM_PROPERTY_FMTID.to_string(),
601 pid: 2,
602 name: "StringProp".to_string(),
603 value: CustomPropertyValue::String("hello".to_string()),
604 },
605 CustomProperty {
606 fmtid: CUSTOM_PROPERTY_FMTID.to_string(),
607 pid: 3,
608 name: "IntProp".to_string(),
609 value: CustomPropertyValue::Int(-7),
610 },
611 CustomProperty {
612 fmtid: CUSTOM_PROPERTY_FMTID.to_string(),
613 pid: 4,
614 name: "FloatProp".to_string(),
615 value: CustomPropertyValue::Float(3.14),
616 },
617 CustomProperty {
618 fmtid: CUSTOM_PROPERTY_FMTID.to_string(),
619 pid: 5,
620 name: "BoolProp".to_string(),
621 value: CustomPropertyValue::Bool(true),
622 },
623 CustomProperty {
624 fmtid: CUSTOM_PROPERTY_FMTID.to_string(),
625 pid: 6,
626 name: "DateProp".to_string(),
627 value: CustomPropertyValue::DateTime("2024-01-01T00:00:00Z".to_string()),
628 },
629 ],
630 };
631
632 let xml = serialize_custom_properties(&props);
633 let parsed = deserialize_custom_properties(&xml).unwrap();
634 assert_eq!(props.properties.len(), parsed.properties.len());
635 for (orig, p) in props.properties.iter().zip(parsed.properties.iter()) {
636 assert_eq!(orig.name, p.name);
637 assert_eq!(orig.value, p.value);
638 }
639 }
640
641 #[test]
642 fn test_custom_properties_empty() {
643 let props = CustomProperties::default();
644 let xml = serialize_custom_properties(&props);
645 let parsed = deserialize_custom_properties(&xml).unwrap();
646 assert!(parsed.properties.is_empty());
647 }
648
649 #[test]
650 fn test_custom_properties_serialized_format() {
651 let props = CustomProperties {
652 properties: vec![CustomProperty {
653 fmtid: CUSTOM_PROPERTY_FMTID.to_string(),
654 pid: 2,
655 name: "MyProp".to_string(),
656 value: CustomPropertyValue::String("MyValue".to_string()),
657 }],
658 };
659
660 let xml = serialize_custom_properties(&props);
661 assert!(xml.contains("<Properties"));
662 assert!(xml.contains("xmlns:vt="));
663 assert!(xml.contains("<property"));
664 assert!(xml.contains("fmtid="));
665 assert!(xml.contains("pid=\"2\""));
666 assert!(xml.contains("name=\"MyProp\""));
667 assert!(xml.contains("<vt:lpwstr>MyValue</vt:lpwstr>"));
668 }
669
670 #[test]
671 fn test_custom_properties_bool_false() {
672 let props = CustomProperties {
673 properties: vec![CustomProperty {
674 fmtid: CUSTOM_PROPERTY_FMTID.to_string(),
675 pid: 2,
676 name: "Flag".to_string(),
677 value: CustomPropertyValue::Bool(false),
678 }],
679 };
680
681 let xml = serialize_custom_properties(&props);
682 let parsed = deserialize_custom_properties(&xml).unwrap();
683 assert_eq!(parsed.properties[0].value, CustomPropertyValue::Bool(false));
684 }
685}