Skip to main content

extend_docx/parse/
content_types.rs

1use std::collections::HashMap;
2
3use crate::package::OoxmlPackage;
4use crate::xml::get_attribute;
5
6use super::context::ContentTypeLookup;
7
8pub fn parse_content_types(pkg: &OoxmlPackage) -> ContentTypeLookup {
9    let mut default_by_extension = HashMap::new();
10    let mut override_by_part_name = HashMap::new();
11
12    let Some(xml) = pkg
13        .parts
14        .get("[Content_Types].xml")
15        .map(|part| part.content.as_str())
16    else {
17        return ContentTypeLookup {
18            default_by_extension,
19            override_by_part_name,
20        };
21    };
22
23    for tag in find_opening_tags(xml, "Default") {
24        let extension = get_attribute(&tag, "Extension").map(|value| value.to_ascii_lowercase());
25        let content_type = get_attribute(&tag, "ContentType");
26        if let (Some(extension), Some(content_type)) = (extension, content_type) {
27            default_by_extension.insert(extension, content_type);
28        }
29    }
30
31    for tag in find_opening_tags(xml, "Override") {
32        let part_name = get_attribute(&tag, "PartName");
33        let content_type = get_attribute(&tag, "ContentType");
34        if let (Some(part_name), Some(content_type)) = (part_name, content_type) {
35            override_by_part_name.insert(part_name.clone(), content_type.clone());
36            if !part_name.starts_with('/') {
37                override_by_part_name.insert(format!("/{part_name}"), content_type);
38            }
39        }
40    }
41
42    ContentTypeLookup {
43        default_by_extension,
44        override_by_part_name,
45    }
46}
47
48fn find_opening_tags(xml: &str, local_name: &str) -> Vec<String> {
49    let open_prefix = format!("<{local_name}");
50    let mut tags = Vec::new();
51    let mut index = 0;
52
53    while index < xml.len() {
54        let Some(tag_start) = find_case_insensitive_at(xml, index, &open_prefix) else {
55            break;
56        };
57
58        if !has_word_boundary_after(xml, tag_start + open_prefix.len()) {
59            index = tag_start + 1;
60            continue;
61        }
62
63        let Some(relative_gt) = xml[tag_start..].find('>') else {
64            break;
65        };
66
67        let tag_end = tag_start + relative_gt + 1;
68        tags.push(xml[tag_start..tag_end].to_string());
69        index = tag_end;
70    }
71
72    tags
73}
74
75fn has_word_boundary_after(xml: &str, index: usize) -> bool {
76    xml.as_bytes()
77        .get(index)
78        .is_none_or(|byte| !byte.is_ascii_alphanumeric() && *byte != b'_')
79}
80
81fn find_case_insensitive_at(haystack: &str, start: usize, needle: &str) -> Option<usize> {
82    crate::xml::find_ignore_ascii_case(haystack, start, needle)
83}