extend_docx/parse/
relationships.rs1use std::collections::HashMap;
2
3use crate::package::{OoxmlPackage, OoxmlPart};
4use crate::parse::context::ContentTypeLookup;
5use crate::xml::{decode_xml_entities, get_attribute, get_attribute_ref};
6
7pub fn mime_by_extension(extension: &str) -> Option<&'static str> {
9 match extension.to_ascii_lowercase().as_str() {
10 "png" => Some("image/png"),
11 "jpg" | "jpeg" => Some("image/jpeg"),
12 "gif" => Some("image/gif"),
13 "bmp" => Some("image/bmp"),
14 "wmf" => Some("image/wmf"),
15 "emf" => Some("image/emf"),
16 "webp" => Some("image/webp"),
17 "svg" => Some("image/svg+xml"),
18 _ => None,
19 }
20}
21
22pub fn resolve_part_path(base_part_name: &str, target: &str) -> String {
24 if target.is_empty() {
25 return String::new();
26 }
27
28 if target
29 .chars()
30 .next()
31 .is_some_and(|ch| ch.is_ascii_alphabetic())
32 && target.contains(':')
33 {
34 return target.to_string();
35 }
36
37 if target.starts_with('#') {
38 return target.to_string();
39 }
40
41 if target.starts_with('/') {
42 return target[1..].to_string();
43 }
44
45 let mut base_segments: Vec<&str> = base_part_name.split('/').collect();
46 if !base_segments.is_empty() {
47 base_segments.pop();
48 }
49
50 let mut output: Vec<String> = base_segments.into_iter().map(str::to_string).collect();
51 for segment in target.split('/') {
52 if segment.is_empty() || segment == "." {
53 continue;
54 }
55 if segment == ".." {
56 output.pop();
57 continue;
58 }
59 output.push(segment.to_string());
60 }
61
62 output.join("/")
63}
64
65pub fn relationship_part_name_for_part(part_name: &str) -> String {
67 let mut segments: Vec<&str> = part_name.split('/').collect();
68 let file_name = segments.pop().unwrap_or("");
69 let folder = segments.join("/");
70 if folder.is_empty() {
71 format!("_rels/{file_name}.rels")
72 } else {
73 format!("{folder}/_rels/{file_name}.rels")
74 }
75}
76
77pub fn parse_part_relationships(pkg: &OoxmlPackage, part_name: &str) -> HashMap<String, String> {
79 parse_relationships_from_parts(&pkg.parts, part_name)
80}
81
82pub fn parse_relationships_from_parts(
84 parts: &HashMap<String, OoxmlPart>,
85 part_name: &str,
86) -> HashMap<String, String> {
87 let mut map = HashMap::new();
88 let relationships_part_name = relationship_part_name_for_part(part_name);
89 let Some(relationships_part) = parts.get(&relationships_part_name) else {
90 return map;
91 };
92
93 for tag in find_opening_tags(&relationships_part.content, "Relationship") {
94 let id = get_attribute(&tag, "Id");
95 let target = get_attribute_ref(&tag, "Target").map(decode_xml_entities);
96 if let (Some(id), Some(target)) = (id, target) {
97 let resolved = if get_attribute_ref(&tag, "TargetMode") == Some("External") {
98 target
99 } else {
100 resolve_part_path(part_name, &target)
101 };
102 map.insert(id, resolved);
103 }
104 }
105
106 map
107}
108
109pub fn extension_from_part_name(part_name: &str) -> Option<String> {
111 let last_dot = part_name.rfind('.')?;
112 if last_dot == part_name.len() - 1 {
113 return None;
114 }
115 Some(part_name[last_dot + 1..].to_ascii_lowercase())
116}
117
118pub fn content_type_for_part(part_name: &str, content_types: &ContentTypeLookup) -> Option<String> {
120 if let Some(override_type) = content_types
121 .override_by_part_name
122 .get(part_name)
123 .or_else(|| {
124 content_types
125 .override_by_part_name
126 .get(&format!("/{part_name}"))
127 })
128 {
129 return Some(override_type.clone());
130 }
131
132 let extension = extension_from_part_name(part_name)?;
133 content_types
134 .default_by_extension
135 .get(&extension)
136 .cloned()
137 .or_else(|| mime_by_extension(&extension).map(str::to_string))
138}
139
140pub fn bytes_to_base64(bytes: &[u8]) -> String {
142 super::util::bytes_to_base64(bytes)
143}
144
145fn find_opening_tags(xml: &str, local_name: &str) -> Vec<String> {
146 let open_prefix = format!("<{local_name}");
147 let mut tags = Vec::new();
148 let mut index = 0;
149
150 while index < xml.len() {
151 let Some(tag_start) = find_case_insensitive_at(xml, index, &open_prefix) else {
152 break;
153 };
154
155 if !has_word_boundary_after(xml, tag_start + open_prefix.len()) {
156 index = tag_start + 1;
157 continue;
158 }
159
160 let Some(relative_gt) = xml[tag_start..].find('>') else {
161 break;
162 };
163
164 let tag_end = tag_start + relative_gt + 1;
165 tags.push(xml[tag_start..tag_end].to_string());
166 index = tag_end;
167 }
168
169 tags
170}
171
172fn has_word_boundary_after(xml: &str, index: usize) -> bool {
173 xml.as_bytes()
174 .get(index)
175 .is_none_or(|byte| !byte.is_ascii_alphanumeric() && *byte != b'_')
176}
177
178fn find_case_insensitive_at(haystack: &str, start: usize, needle: &str) -> Option<usize> {
179 crate::xml::find_ignore_ascii_case(haystack, start, needle)
180}