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