Skip to main content

oxml_opc/
content_types.rs

1//! Parsing and writing of `[Content_Types].xml`.
2
3use std::collections::HashMap;
4
5use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
6use quick_xml::{Reader, Writer};
7
8use crate::error::{OpcError, Result};
9
10pub const RELATIONSHIPS: &str = "application/vnd.openxmlformats-package.relationships+xml";
11pub const XML: &str = "application/xml";
12
13pub const CORE_PROPERTIES: &str = "application/vnd.openxmlformats-package.core-properties+xml";
14pub const EXTENDED_PROPERTIES: &str =
15    "application/vnd.openxmlformats-officedocument.extended-properties+xml";
16pub const CUSTOM_PROPERTIES: &str =
17    "application/vnd.openxmlformats-officedocument.custom-properties+xml";
18pub const THEME: &str = "application/vnd.openxmlformats-officedocument.theme+xml";
19pub const CHART: &str = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml";
20
21pub const PRESENTATION: &str =
22    "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml";
23pub const SLIDESHOW: &str =
24    "application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml";
25pub const SLIDE: &str = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml";
26pub const SLIDE_LAYOUT: &str =
27    "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml";
28pub const SLIDE_MASTER: &str =
29    "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml";
30pub const NOTES_SLIDE: &str =
31    "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml";
32pub const NOTES_MASTER: &str =
33    "application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml";
34pub const PRES_PROPS: &str =
35    "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml";
36pub const VIEW_PROPS: &str =
37    "application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml";
38pub const TABLE_STYLES: &str =
39    "application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml";
40pub const HANDOUT_MASTER: &str =
41    "application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml";
42
43pub const WORKBOOK: &str =
44    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml";
45pub const EMBEDDED_WORKBOOK: &str =
46    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
47pub const WORKSHEET: &str =
48    "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml";
49pub const SHARED_STRINGS: &str =
50    "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml";
51pub const STYLES: &str = "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml";
52
53/// A single content type entry, either a Default by extension or an Override by part name.
54#[derive(Debug, Clone, PartialEq)]
55pub enum ContentType {
56    Default {
57        extension: String,
58        content_type: String,
59    },
60    Override {
61        part_name: String,
62        content_type: String,
63    },
64}
65
66/// Parsed `[Content_Types].xml`.
67#[derive(Debug, Clone)]
68pub struct ContentTypes {
69    pub defaults: HashMap<String, String>,
70    pub overrides: HashMap<String, String>,
71}
72
73impl ContentTypes {
74    /// Parse from XML bytes.
75    pub fn from_xml(xml: &[u8]) -> Result<Self> {
76        let mut reader = Reader::from_reader(xml);
77        reader.config_mut().trim_text(true);
78
79        let mut defaults = HashMap::new();
80        let mut overrides = HashMap::new();
81        let mut buf = Vec::new();
82
83        loop {
84            match reader.read_event_into(&mut buf) {
85                Ok(Event::Empty(ref e)) => match e.name().as_ref() {
86                    b"Default" => {
87                        let mut ext = None;
88                        let mut ct = None;
89                        for attr in e.attributes() {
90                            let attr = attr?;
91                            match attr.key.as_ref() {
92                                b"Extension" => {
93                                    ext = Some(std::str::from_utf8(&attr.value)?.to_string());
94                                }
95                                b"ContentType" => {
96                                    ct = Some(std::str::from_utf8(&attr.value)?.to_string());
97                                }
98                                _ => {}
99                            }
100                        }
101                        match (ext, ct) {
102                            (Some(e), Some(c)) => {
103                                defaults.insert(e, c);
104                            }
105                            _ => return Err(OpcError::InvalidContentTypes),
106                        }
107                    }
108                    b"Override" => {
109                        let mut pn = None;
110                        let mut ct = None;
111                        for attr in e.attributes() {
112                            let attr = attr?;
113                            match attr.key.as_ref() {
114                                b"PartName" => {
115                                    pn = Some(std::str::from_utf8(&attr.value)?.to_string());
116                                }
117                                b"ContentType" => {
118                                    ct = Some(std::str::from_utf8(&attr.value)?.to_string());
119                                }
120                                _ => {}
121                            }
122                        }
123                        match (pn, ct) {
124                            (Some(p), Some(c)) => {
125                                overrides.insert(p, c);
126                            }
127                            _ => return Err(OpcError::InvalidContentTypes),
128                        }
129                    }
130                    _ => {}
131                },
132                Ok(Event::Eof) => break,
133                Err(e) => return Err(e.into()),
134                _ => {}
135            }
136            buf.clear();
137        }
138
139        Ok(ContentTypes {
140            defaults,
141            overrides,
142        })
143    }
144
145    /// Serialize to XML bytes.
146    pub fn to_xml(&self) -> Result<Vec<u8>> {
147        let mut writer = Writer::new_with_indent(Vec::new(), b' ', 2);
148
149        writer.write_event(Event::Decl(BytesDecl::new(
150            "1.0",
151            Some("UTF-8"),
152            Some("yes"),
153        )))?;
154
155        let mut types_start = BytesStart::new("Types");
156        types_start.push_attribute((
157            "xmlns",
158            "http://schemas.openxmlformats.org/package/2006/content-types",
159        ));
160        writer.write_event(Event::Start(types_start))?;
161
162        // Write defaults sorted for deterministic output
163        let mut sorted_defaults: Vec<_> = self.defaults.iter().collect();
164        sorted_defaults.sort_by_key(|(k, _)| (*k).clone());
165        for (ext, ct) in sorted_defaults {
166            let mut elem = BytesStart::new("Default");
167            elem.push_attribute(("Extension", ext.as_str()));
168            elem.push_attribute(("ContentType", ct.as_str()));
169            writer.write_event(Event::Empty(elem))?;
170        }
171
172        // Write overrides sorted for deterministic output
173        let mut sorted_overrides: Vec<_> = self.overrides.iter().collect();
174        sorted_overrides.sort_by_key(|(k, _)| (*k).clone());
175        for (pn, ct) in sorted_overrides {
176            let mut elem = BytesStart::new("Override");
177            elem.push_attribute(("PartName", pn.as_str()));
178            elem.push_attribute(("ContentType", ct.as_str()));
179            writer.write_event(Event::Empty(elem))?;
180        }
181
182        writer.write_event(Event::End(BytesEnd::new("Types")))?;
183
184        Ok(writer.into_inner())
185    }
186
187    /// Look up the content type for a given part name.
188    pub fn content_type_for(&self, part_name: &str) -> Option<&str> {
189        // Check overrides first
190        if let Some(ct) = self.overrides.get(part_name) {
191            return Some(ct.as_str());
192        }
193        // Fall back to defaults by extension
194        if let Some(dot_pos) = part_name.rfind('.') {
195            let ext = &part_name[dot_pos + 1..];
196            if let Some(ct) = self.defaults.get(ext) {
197                return Some(ct.as_str());
198            }
199        }
200        None
201    }
202
203    /// Add a default content type for an extension (e.g., "png" -> "image/png").
204    pub fn add_default(&mut self, extension: &str, content_type: &str) {
205        self.defaults
206            .entry(extension.to_string())
207            .or_insert_with(|| content_type.to_string());
208    }
209
210    /// Add an override content type for a specific part name.
211    pub fn add_override(&mut self, part_name: &str, content_type: &str) {
212        self.overrides
213            .insert(part_name.to_string(), content_type.to_string());
214    }
215
216    /// Create the minimal content types shared by every OPC package.
217    pub fn minimal() -> Self {
218        let mut defaults = HashMap::new();
219        defaults.insert("rels".to_string(), RELATIONSHIPS.to_string());
220        defaults.insert("xml".to_string(), XML.to_string());
221
222        ContentTypes {
223            defaults,
224            overrides: HashMap::new(),
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    fn docx_content_types() -> ContentTypes {
234        let mut content_types = ContentTypes::minimal();
235        content_types.add_override(
236            "/word/document.xml",
237            "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
238        );
239        content_types.add_override(
240            "/word/styles.xml",
241            "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml",
242        );
243        content_types
244    }
245
246    #[test]
247    fn minimal_content_types_contain_only_universal_defaults() {
248        let content_types = ContentTypes::minimal();
249
250        assert_eq!(content_types.defaults.len(), 2);
251        assert_eq!(
252            content_types.defaults.get("rels").map(String::as_str),
253            Some("application/vnd.openxmlformats-package.relationships+xml")
254        );
255        assert_eq!(
256            content_types.defaults.get("xml").map(String::as_str),
257            Some("application/xml")
258        );
259        assert!(content_types.overrides.is_empty());
260    }
261
262    #[test]
263    fn round_trip_content_types() {
264        let ct = docx_content_types();
265        let xml = ct.to_xml().unwrap();
266        let parsed = ContentTypes::from_xml(&xml).unwrap();
267        assert_eq!(parsed.defaults.len(), ct.defaults.len());
268        assert_eq!(parsed.overrides.len(), ct.overrides.len());
269        assert_eq!(
270            parsed.content_type_for("/word/document.xml"),
271            Some(
272                "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"
273            )
274        );
275    }
276
277    #[test]
278    fn lookup_by_extension() {
279        let ct = docx_content_types();
280        assert_eq!(
281            ct.content_type_for("/word/_rels/document.xml.rels"),
282            Some("application/vnd.openxmlformats-package.relationships+xml")
283        );
284    }
285}