1use std::io::Write;
4
5use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
6use quick_xml::name::{Namespace, ResolveResult};
7use quick_xml::reader::NsReader;
8use quick_xml::{Reader, Writer, XmlVersion};
9
10use crate::error::{OxmlError, Result};
11use crate::raw_xml::{capture_element, capture_empty_element};
12use crate::xml::{extra_namespace_declarations, local_name};
13use crate::xml_text::read_element_text;
14
15const CUSTOM_PROPERTIES_NS: &str =
16 "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties";
17const VARIANT_TYPES_NS: &str =
18 "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes";
19
20#[derive(Debug, Clone, PartialEq)]
22pub enum CustomPropertyValue {
23 Lpstr(String),
25 Lpwstr(String),
27 I4(i32),
29 R8(f64),
31 Bool(bool),
33 FileTime(String),
35 Empty,
37 Raw(Vec<u8>),
39}
40
41#[derive(Debug, Clone, PartialEq)]
43pub struct CustomProperty {
44 pub fmtid: String,
45 pub pid: i32,
46 pub name: Option<String>,
47 pub value: CustomPropertyValue,
48}
49
50#[derive(Debug, Clone, Default, PartialEq)]
52pub struct CustomProperties {
53 pub properties: Vec<CustomProperty>,
54 extra_namespaces: Vec<(String, String)>,
55}
56
57impl CustomProperties {
58 pub fn from_xml(xml: &[u8]) -> Result<Self> {
60 if !custom_property_namespaces_are_valid(xml) {
61 return Err(OxmlError::UnexpectedElement(
62 "custom properties namespace".to_owned(),
63 ));
64 }
65 let mut reader = Reader::from_reader(xml);
66 let mut properties = Self::default();
67 let mut root_open = false;
68 let mut root_closed = false;
69 let mut buf = Vec::new();
70
71 loop {
72 match reader.read_event_into(&mut buf) {
73 Ok(Event::Start(ref element)) => {
74 let qualified_name = element.name();
75 let name = local_name(qualified_name.as_ref());
76 if name == b"Properties" {
77 if root_open || root_closed {
78 return Err(OxmlError::UnexpectedElement("Properties".to_owned()));
79 }
80 root_open = true;
81 properties
82 .extra_namespaces
83 .extend(extra_namespace_declarations(element)?);
84 } else if !root_open {
85 return Err(OxmlError::UnexpectedElement(
86 String::from_utf8_lossy(name).into_owned(),
87 ));
88 } else if name == b"property" {
89 properties
90 .properties
91 .push(parse_property(&mut reader, element)?);
92 } else {
93 return Err(OxmlError::UnexpectedElement(
94 String::from_utf8_lossy(name).into_owned(),
95 ));
96 }
97 }
98 Ok(Event::Empty(ref element)) => {
99 let qualified_name = element.name();
100 let name = local_name(qualified_name.as_ref());
101 if name == b"Properties" {
102 if root_open || root_closed {
103 return Err(OxmlError::UnexpectedElement("Properties".to_owned()));
104 }
105 root_closed = true;
106 } else if !root_open {
107 return Err(OxmlError::UnexpectedElement(
108 String::from_utf8_lossy(name).into_owned(),
109 ));
110 } else if name == b"property" {
111 return Err(OxmlError::MissingElement(
112 "custom property value".to_owned(),
113 ));
114 } else {
115 return Err(OxmlError::UnexpectedElement(
116 String::from_utf8_lossy(name).into_owned(),
117 ));
118 }
119 }
120 Ok(Event::End(ref element))
121 if local_name(element.name().as_ref()) == b"Properties" =>
122 {
123 if !root_open {
124 return Err(OxmlError::UnexpectedElement("Properties".to_owned()));
125 }
126 root_open = false;
127 root_closed = true;
128 }
129 Ok(Event::Eof) => break,
130 Err(error) => return Err(error.into()),
131 _ => {}
132 }
133 buf.clear();
134 }
135
136 if root_closed {
137 Ok(properties)
138 } else {
139 Err(OxmlError::MissingElement("Properties root".to_owned()))
140 }
141 }
142
143 pub fn to_xml(&self) -> Result<Vec<u8>> {
145 let mut writer = Writer::new(Vec::new());
146 writer.write_event(Event::Decl(BytesDecl::new(
147 "1.0",
148 Some("UTF-8"),
149 Some("yes"),
150 )))?;
151
152 let mut root = BytesStart::new("Properties");
153 root.push_attribute(("xmlns", CUSTOM_PROPERTIES_NS));
154 root.push_attribute(("xmlns:vt", VARIANT_TYPES_NS));
155 for (name, value) in &self.extra_namespaces {
156 root.push_attribute((name.as_str(), value.as_str()));
157 }
158 writer.write_event(Event::Start(root))?;
159
160 for property in &self.properties {
161 write_property(&mut writer, property)?;
162 }
163
164 writer.write_event(Event::End(BytesEnd::new("Properties")))?;
165 Ok(writer.into_inner())
166 }
167}
168
169fn custom_property_namespaces_are_valid(xml: &[u8]) -> bool {
170 let mut reader = NsReader::from_reader(xml);
171 let mut buffer = Vec::new();
172 let mut depth = 0usize;
173 let mut saw_root = false;
174 loop {
175 let Ok((namespace, event)) = reader.read_resolved_event_into(&mut buffer) else {
176 return false;
177 };
178 let is_start = matches!(event, Event::Start(_));
179 if let Event::Start(ref element) | Event::Empty(ref element) = event {
180 let valid = match depth {
181 0 => {
182 matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == CUSTOM_PROPERTIES_NS.as_bytes())
183 && local_name(element.name().as_ref()) == b"Properties"
184 }
185 1 => {
186 matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == CUSTOM_PROPERTIES_NS.as_bytes())
187 && local_name(element.name().as_ref()) == b"property"
188 && property_attributes_are_unqualified(&reader, element)
189 }
190 2 => {
191 matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == VARIANT_TYPES_NS.as_bytes())
192 }
193 _ => true,
194 };
195 if !valid {
196 return false;
197 }
198 saw_root = true;
199 if is_start {
200 let Some(next_depth) = depth.checked_add(1) else {
201 return false;
202 };
203 depth = next_depth;
204 }
205 } else if matches!(event, Event::End(_)) {
206 depth = depth.saturating_sub(1);
207 } else if matches!(event, Event::Eof) {
208 return saw_root && depth == 0;
209 }
210 buffer.clear();
211 }
212}
213
214fn property_attributes_are_unqualified(reader: &NsReader<&[u8]>, element: &BytesStart<'_>) -> bool {
215 element.attributes().all(|attribute| {
216 let Ok(attribute) = attribute else {
217 return false;
218 };
219 let name = attribute.key.as_ref();
220 (name == b"xmlns" || name.starts_with(b"xmlns:"))
221 || matches!(
222 reader.resolver().resolve_attribute(attribute.key).0,
223 ResolveResult::Unbound
224 )
225 })
226}
227
228fn parse_property(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<CustomProperty> {
229 let fmtid = required_attribute(start, b"fmtid")?;
230 let pid_text = required_attribute(start, b"pid")?;
231 let pid = pid_text.parse().map_err(|_| {
232 OxmlError::InvalidValue(format!(
233 "custom property pid must be an integer, got {pid_text:?}"
234 ))
235 })?;
236 let name = optional_attribute(start, b"name")?;
237 let mut value = None;
238 let mut buf = Vec::new();
239
240 loop {
241 match reader.read_event_into(&mut buf) {
242 Ok(Event::Start(ref element)) => {
243 if value.is_some() {
244 return Err(OxmlError::InvalidValue(
245 "custom property has more than one value".to_owned(),
246 ));
247 }
248 value = Some(parse_value_element(reader, element)?);
249 }
250 Ok(Event::Empty(ref element)) => {
251 if value.is_some() {
252 return Err(OxmlError::InvalidValue(
253 "custom property has more than one value".to_owned(),
254 ));
255 }
256 value = Some(parse_empty_value(element)?);
257 }
258 Ok(Event::End(ref element)) if local_name(element.name().as_ref()) == b"property" => {
259 break;
260 }
261 Ok(Event::Eof) => break,
262 Err(error) => return Err(error.into()),
263 _ => {}
264 }
265 buf.clear();
266 }
267
268 Ok(CustomProperty {
269 fmtid,
270 pid,
271 name,
272 value: value
273 .ok_or_else(|| OxmlError::MissingElement("custom property value".to_owned()))?,
274 })
275}
276
277fn parse_value_element(
278 reader: &mut Reader<&[u8]>,
279 element: &BytesStart<'_>,
280) -> Result<CustomPropertyValue> {
281 let qualified_name = element.name();
282 let name = local_name(qualified_name.as_ref());
283 match name {
284 b"lpstr" => Ok(CustomPropertyValue::Lpstr(read_element_text(
285 reader,
286 element.name(),
287 ))),
288 b"lpwstr" => Ok(CustomPropertyValue::Lpwstr(read_element_text(
289 reader,
290 element.name(),
291 ))),
292 b"i4" => {
293 let text = read_element_text(reader, element.name());
294 let value = text.trim().parse().map_err(|_| {
295 OxmlError::InvalidValue(format!("vt:i4 must be an integer, got {text:?}"))
296 })?;
297 Ok(CustomPropertyValue::I4(value))
298 }
299 b"r8" => {
300 let text = read_element_text(reader, element.name());
301 Ok(CustomPropertyValue::R8(parse_r8(&text)?))
302 }
303 b"bool" => {
304 let text = read_element_text(reader, element.name());
305 Ok(CustomPropertyValue::Bool(parse_bool(&text)?))
306 }
307 b"filetime" => Ok(CustomPropertyValue::FileTime(read_element_text(
308 reader,
309 element.name(),
310 ))),
311 b"empty" => {
312 reader.read_to_end_into(element.name(), &mut Vec::new())?;
313 Ok(CustomPropertyValue::Empty)
314 }
315 _ => Ok(CustomPropertyValue::Raw(capture_element(reader, element)?)),
316 }
317}
318
319fn parse_empty_value(element: &BytesStart<'_>) -> Result<CustomPropertyValue> {
320 match local_name(element.name().as_ref()) {
321 b"lpstr" => Ok(CustomPropertyValue::Lpstr(String::new())),
322 b"lpwstr" => Ok(CustomPropertyValue::Lpwstr(String::new())),
323 b"i4" => Err(OxmlError::InvalidValue(
324 "vt:i4 must be an integer, got an empty value".to_owned(),
325 )),
326 b"r8" => Err(OxmlError::InvalidValue(
327 "vt:r8 must be a number, got an empty value".to_owned(),
328 )),
329 b"bool" => Err(OxmlError::InvalidValue(
330 "vt:bool must be a Boolean, got an empty value".to_owned(),
331 )),
332 b"filetime" => Ok(CustomPropertyValue::FileTime(String::new())),
333 b"empty" => Ok(CustomPropertyValue::Empty),
334 _ => Ok(CustomPropertyValue::Raw(capture_empty_element(element)?)),
335 }
336}
337
338fn optional_attribute(element: &BytesStart<'_>, expected: &[u8]) -> Result<Option<String>> {
339 for attribute in element.attributes() {
340 let attribute = attribute?;
341 if local_name(attribute.key.as_ref()) == expected {
342 return Ok(Some(
343 attribute
344 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?
345 .into_owned(),
346 ));
347 }
348 }
349 Ok(None)
350}
351
352fn required_attribute(element: &BytesStart<'_>, expected: &[u8]) -> Result<String> {
353 optional_attribute(element, expected)?.ok_or_else(|| {
354 OxmlError::MissingElement(format!(
355 "custom property {} attribute",
356 String::from_utf8_lossy(expected)
357 ))
358 })
359}
360
361fn parse_bool(text: &str) -> Result<bool> {
362 match text.trim() {
363 "true" | "1" => Ok(true),
364 "false" | "0" => Ok(false),
365 _ => Err(OxmlError::InvalidValue(format!(
366 "vt:bool must be a Boolean, got {text:?}"
367 ))),
368 }
369}
370
371fn parse_r8(text: &str) -> Result<f64> {
372 match text.trim() {
373 "INF" => Ok(f64::INFINITY),
374 "-INF" => Ok(f64::NEG_INFINITY),
375 "NaN" => Ok(f64::NAN),
376 value => value
377 .parse()
378 .map_err(|_| OxmlError::InvalidValue(format!("vt:r8 must be a number, got {text:?}"))),
379 }
380}
381
382fn write_property(writer: &mut Writer<Vec<u8>>, property: &CustomProperty) -> Result<()> {
383 let mut start = BytesStart::new("property");
384 start.push_attribute(("fmtid", property.fmtid.as_str()));
385 let pid = property.pid.to_string();
386 start.push_attribute(("pid", pid.as_str()));
387 if let Some(name) = &property.name {
388 start.push_attribute(("name", name.as_str()));
389 }
390 writer.write_event(Event::Start(start))?;
391 write_value(writer, &property.value)?;
392 writer.write_event(Event::End(BytesEnd::new("property")))?;
393 Ok(())
394}
395
396fn write_value(writer: &mut Writer<Vec<u8>>, value: &CustomPropertyValue) -> Result<()> {
397 match value {
398 CustomPropertyValue::Lpstr(value) => write_text(writer, "vt:lpstr", value),
399 CustomPropertyValue::Lpwstr(value) => write_text(writer, "vt:lpwstr", value),
400 CustomPropertyValue::I4(value) => write_text(writer, "vt:i4", &value.to_string()),
401 CustomPropertyValue::R8(value) => write_text(writer, "vt:r8", r8_text(*value).as_ref()),
402 CustomPropertyValue::Bool(value) => write_text(writer, "vt:bool", &value.to_string()),
403 CustomPropertyValue::FileTime(value) => write_text(writer, "vt:filetime", value),
404 CustomPropertyValue::Empty => {
405 writer.write_event(Event::Empty(BytesStart::new("vt:empty")))?;
406 Ok(())
407 }
408 CustomPropertyValue::Raw(raw) => {
409 writer.get_mut().write_all(raw)?;
410 Ok(())
411 }
412 }
413}
414
415fn r8_text(value: f64) -> String {
416 if value.is_nan() {
417 "NaN".to_owned()
418 } else if value == f64::INFINITY {
419 "INF".to_owned()
420 } else if value == f64::NEG_INFINITY {
421 "-INF".to_owned()
422 } else {
423 value.to_string()
424 }
425}
426
427fn write_text(writer: &mut Writer<Vec<u8>>, tag: &str, value: &str) -> Result<()> {
428 writer.write_event(Event::Start(BytesStart::new(tag)))?;
429 writer.write_event(Event::Text(BytesText::new(value)))?;
430 writer.write_event(Event::End(BytesEnd::new(tag)))?;
431 Ok(())
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 const FMTID: &str = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}";
439
440 #[test]
441 fn custom_property_value_types_round_trip() {
442 let xml = format!(
443 r#"<Properties xmlns="{CUSTOM_PROPERTIES_NS}" xmlns:vt="{VARIANT_TYPES_NS}">
444<property fmtid="{FMTID}" pid="2" name="Wide & text"><vt:lpwstr>hello & goodbye</vt:lpwstr></property>
445<property fmtid="{FMTID}" pid="3" name="Narrow"><vt:lpstr>plain</vt:lpstr></property>
446<property fmtid="{FMTID}" pid="4" name="Count"><vt:i4>-42</vt:i4></property>
447<property fmtid="{FMTID}" pid="5" name="Ratio"><vt:r8>1.25</vt:r8></property>
448<property fmtid="{FMTID}" pid="6" name="Flag"><vt:bool>1</vt:bool></property>
449<property fmtid="{FMTID}" pid="7" name="When"><vt:filetime>2026-07-30T08:15:00Z</vt:filetime></property>
450<property fmtid="{FMTID}" pid="8" name="Nothing"><vt:empty/></property>
451</Properties>"#
452 );
453 let properties = CustomProperties::from_xml(xml.as_bytes()).unwrap();
454 assert_eq!(properties.properties.len(), 7);
455 assert_eq!(
456 properties.properties[0].name.as_deref(),
457 Some("Wide & text")
458 );
459 assert_eq!(
460 properties.properties[0].value,
461 CustomPropertyValue::Lpwstr("hello & goodbye".to_owned())
462 );
463 assert_eq!(properties.properties[2].value, CustomPropertyValue::I4(-42));
464 assert_eq!(
465 properties.properties[4].value,
466 CustomPropertyValue::Bool(true)
467 );
468 assert_eq!(properties.properties[6].value, CustomPropertyValue::Empty);
469
470 let output = properties.to_xml().unwrap();
471 assert_eq!(CustomProperties::from_xml(&output).unwrap(), properties);
472 }
473
474 #[test]
475 fn unknown_custom_property_value_is_preserved_verbatim() {
476 let xml = format!(
477 r#"<Properties xmlns="{CUSTOM_PROPERTIES_NS}" xmlns:v="{VARIANT_TYPES_NS}"><property fmtid="{FMTID}" pid="2" name="Unsigned"><v:ui4>4294967295</v:ui4></property></Properties>"#
478 );
479 let properties = CustomProperties::from_xml(xml.as_bytes()).unwrap();
480 let expected = br#"<v:ui4>4294967295</v:ui4>"#;
481 assert_eq!(
482 properties.properties[0].value,
483 CustomPropertyValue::Raw(expected.to_vec())
484 );
485 let output = properties.to_xml().unwrap();
486 assert!(
487 output
488 .windows(expected.len())
489 .any(|window| window == expected)
490 );
491 assert!(
492 std::str::from_utf8(&output)
493 .unwrap()
494 .contains(&format!(r#"xmlns:v="{VARIANT_TYPES_NS}""#))
495 );
496 assert_eq!(CustomProperties::from_xml(&output).unwrap(), properties);
497 }
498
499 #[test]
500 fn malformed_custom_properties_are_rejected() {
501 assert!(CustomProperties::from_xml(b"").is_err());
502 assert!(CustomProperties::from_xml(b"<Wrong/>").is_err());
503 assert!(CustomProperties::from_xml(b"<Properties>").is_err());
504
505 let missing_pid = format!(
506 r#"<Properties xmlns="{CUSTOM_PROPERTIES_NS}" xmlns:vt="{VARIANT_TYPES_NS}"><property fmtid="{FMTID}"><vt:i4>1</vt:i4></property></Properties>"#
507 );
508 assert!(CustomProperties::from_xml(missing_pid.as_bytes()).is_err());
509
510 let two_values = format!(
511 r#"<Properties xmlns="{CUSTOM_PROPERTIES_NS}" xmlns:vt="{VARIANT_TYPES_NS}"><property fmtid="{FMTID}" pid="2"><vt:i4>1</vt:i4><vt:i4>2</vt:i4></property></Properties>"#
512 );
513 assert!(CustomProperties::from_xml(two_values.as_bytes()).is_err());
514
515 for value in ["<vt:i4/>", "<vt:r8/>", "<vt:bool/>"] {
516 let empty_typed_value = format!(
517 r#"<Properties xmlns="{CUSTOM_PROPERTIES_NS}" xmlns:vt="{VARIANT_TYPES_NS}"><property fmtid="{FMTID}" pid="2">{value}</property></Properties>"#
518 );
519 assert!(CustomProperties::from_xml(empty_typed_value.as_bytes()).is_err());
520 }
521 }
522
523 #[test]
524 fn custom_property_projection_requires_expanded_names() {
525 let aliased = format!(
526 r#"<p:Properties xmlns:p="{CUSTOM_PROPERTIES_NS}" xmlns:v="{VARIANT_TYPES_NS}"><p:property fmtid="{FMTID}" pid="2" name="Tier"><v:lpwstr>Gold</v:lpwstr></p:property></p:Properties>"#
527 );
528 assert_eq!(
529 CustomProperties::from_xml(aliased.as_bytes())
530 .unwrap()
531 .properties[0]
532 .value,
533 CustomPropertyValue::Lpwstr("Gold".to_owned())
534 );
535 for foreign in [
536 r#"<x:Properties xmlns:x="urn:foreign"/>"#.to_owned(),
537 format!(
538 r#"<p:Properties xmlns:p="{CUSTOM_PROPERTIES_NS}" xmlns:v="{VARIANT_TYPES_NS}" xmlns:x="urn:foreign"><x:property fmtid="{FMTID}" pid="2"><v:i4>1</v:i4></x:property></p:Properties>"#
539 ),
540 format!(
541 r#"<p:Properties xmlns:p="{CUSTOM_PROPERTIES_NS}" xmlns:v="{VARIANT_TYPES_NS}" xmlns:x="urn:foreign"><p:property fmtid="{FMTID}" pid="2"><x:i4>1</x:i4></p:property></p:Properties>"#
542 ),
543 format!(
544 r#"<p:Properties xmlns:p="{CUSTOM_PROPERTIES_NS}" xmlns:v="{VARIANT_TYPES_NS}" xmlns:x="urn:foreign"><p:property x:fmtid="{FMTID}" x:pid="2"><v:i4>1</v:i4></p:property></p:Properties>"#
545 ),
546 ] {
547 assert!(CustomProperties::from_xml(foreign.as_bytes()).is_err());
548 }
549 }
550}