1use std::{cell::RefCell, collections::HashMap, marker::PhantomData};
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5mod byte_asset_map {
6 use super::*;
7
8 pub fn serialize<S>(map: &HashMap<String, Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
9 where
10 S: Serializer,
11 {
12 use serde::ser::SerializeMap;
13 let mut ser_map = serializer.serialize_map(Some(map.len()))?;
14 for (key, value) in map {
15 ser_map.serialize_entry(key, &serde_bytes::Bytes::new(value))?;
16 }
17 ser_map.end()
18 }
19
20 pub fn deserialize<'de, D>(deserializer: D) -> Result<HashMap<String, Vec<u8>>, D::Error>
21 where
22 D: Deserializer<'de>,
23 {
24 let raw = HashMap::<String, serde_bytes::ByteBuf>::deserialize(deserializer)?;
25 Ok(raw
26 .into_iter()
27 .map(|(key, value)| (key, value.into_vec()))
28 .collect())
29 }
30}
31
32#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct OoxmlPart {
36 pub name: String,
37 pub content: String,
38}
39
40#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub struct OoxmlPackage {
44 pub parts: HashMap<String, OoxmlPart>,
45 #[serde(with = "byte_asset_map")]
46 pub binary_assets: HashMap<String, Vec<u8>>,
47 #[serde(default, skip_serializing_if = "Vec::is_empty")]
51 pub warnings: Vec<String>,
52}
53
54pub fn get_part<'a>(pkg: &'a OoxmlPackage, part_name: &str) -> Option<&'a OoxmlPart> {
55 pkg.parts.get(part_name)
56}
57
58pub fn with_part(pkg: &OoxmlPackage, part: OoxmlPart) -> OoxmlPackage {
59 let mut parts = pkg.parts.clone();
60 parts.insert(part.name.clone(), part);
61 OoxmlPackage {
62 parts,
63 binary_assets: pkg.binary_assets.clone(),
64 warnings: pkg.warnings.clone(),
65 }
66}
67
68struct RelationshipTargets {
69 package_address: usize,
70 sources: HashMap<String, HashMap<String, Option<String>>>,
71}
72
73thread_local! {
74 static RELATIONSHIP_TARGETS: RefCell<Option<RelationshipTargets>> = const { RefCell::new(None) };
75}
76
77pub(crate) struct RelationshipCacheScope<'a> {
79 previous: Option<RelationshipTargets>,
80 package: PhantomData<std::rc::Rc<&'a OoxmlPackage>>,
81}
82
83pub(crate) fn relationship_cache_scope(pkg: &OoxmlPackage) -> RelationshipCacheScope<'_> {
84 let targets = RelationshipTargets {
85 package_address: pkg as *const OoxmlPackage as usize,
86 sources: HashMap::new(),
87 };
88 RelationshipCacheScope {
89 previous: RELATIONSHIP_TARGETS.with(|current| current.replace(Some(targets))),
90 package: PhantomData,
91 }
92}
93
94impl Drop for RelationshipCacheScope<'_> {
95 fn drop(&mut self) {
96 RELATIONSHIP_TARGETS.with(|current| current.replace(self.previous.take()));
97 }
98}
99
100pub fn relationship_target(pkg: &OoxmlPackage, source: &str, kind: &str) -> Option<String> {
101 enum Cached {
102 Unscoped,
103 SourceMissing,
104 Target(Option<String>),
105 }
106 let address = pkg as *const OoxmlPackage as usize;
107 let cached = RELATIONSHIP_TARGETS.with(|current| {
108 let current = current.borrow();
109 let Some(targets) = current
110 .as_ref()
111 .filter(|targets| targets.package_address == address)
112 else {
113 return Cached::Unscoped;
114 };
115 match targets.sources.get(source) {
116 Some(targets) => Cached::Target(targets.get(kind).cloned().flatten()),
117 None => Cached::SourceMissing,
118 }
119 });
120 match cached {
121 Cached::Unscoped => uncached_relationship_target(pkg, source, kind),
122 Cached::Target(target) => target,
123 Cached::SourceMissing => {
124 let targets = collect_relationship_targets(pkg, source);
125 let target = targets.get(kind).cloned().flatten();
126 RELATIONSHIP_TARGETS.with(|current| {
127 let mut current = current.borrow_mut();
128 if let Some(cache) = current
129 .as_mut()
130 .filter(|cache| cache.package_address == address)
131 {
132 cache.sources.insert(source.to_owned(), targets);
133 }
134 });
135 target
136 }
137 }
138}
139
140fn collect_relationship_targets(
141 pkg: &OoxmlPackage,
142 source: &str,
143) -> HashMap<String, Option<String>> {
144 let rel_name = if source.is_empty() {
145 "_rels/.rels".to_string()
146 } else {
147 crate::parse::relationship_part_name_for_part(source)
148 };
149 let Some(part) = pkg.parts.get(&rel_name) else {
150 return HashMap::new();
151 };
152 let Ok(xml) = roxmltree::Document::parse(&part.content) else {
153 return HashMap::new();
154 };
155 let mut targets = HashMap::new();
156 for rel in xml.descendants().filter(|node| {
157 node.is_element()
158 && node.tag_name().name() == "Relationship"
159 && node.attribute("TargetMode") != Some("External")
160 }) {
161 let Some(kind) = rel
162 .attribute("Type")
163 .and_then(|value| value.rsplit('/').next())
164 else {
165 continue;
166 };
167 if !targets.contains_key(kind) {
169 targets.insert(
170 kind.to_owned(),
171 rel.attribute("Target")
172 .map(|target| crate::parse::resolve_part_path(source, target)),
173 );
174 }
175 }
176 targets
177}
178
179fn uncached_relationship_target(pkg: &OoxmlPackage, source: &str, kind: &str) -> Option<String> {
180 let rel_name = if source.is_empty() {
181 "_rels/.rels".to_string()
182 } else {
183 crate::parse::relationship_part_name_for_part(source)
184 };
185 let part = pkg.parts.get(&rel_name)?;
186 let xml = roxmltree::Document::parse(&part.content).ok()?;
187 let rel = xml.descendants().find(|node| {
188 node.is_element()
189 && node.tag_name().name() == "Relationship"
190 && node
191 .attribute("Type")
192 .is_some_and(|value| value.rsplit('/').next() == Some(kind))
193 && node.attribute("TargetMode") != Some("External")
194 })?;
195 Some(crate::parse::resolve_part_path(
196 source,
197 rel.attribute("Target")?,
198 ))
199}
200
201pub fn main_document_part_name(pkg: &OoxmlPackage) -> String {
202 relationship_target(pkg, "", "officeDocument").unwrap_or_else(|| "word/document.xml".into())
203}
204
205pub fn related_document_part_name(pkg: &OoxmlPackage, kind: &str) -> String {
206 relationship_target(pkg, &main_document_part_name(pkg), kind).unwrap_or_else(|| {
207 if kind == "theme" {
208 "word/theme/theme1.xml".into()
209 } else {
210 format!("word/{kind}.xml")
211 }
212 })
213}
214
215pub fn related_document_part<'a>(pkg: &'a OoxmlPackage, kind: &str) -> Option<&'a OoxmlPart> {
216 pkg.parts.get(&related_document_part_name(pkg, kind))
217}