Skip to main content

extend_docx/
package.rs

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/// Mirrors TypeScript `OoxmlPart`.
33#[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/// Mirrors TypeScript `OoxmlPackage`.
41#[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    /// Import diagnostics that cannot be represented by OOXML itself. This is
48    /// empty for native DOCX packages and populated by format adapters such as
49    /// the legacy binary DOC importer.
50    #[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
77// The package borrow and thread affinity keep address-based cache keys valid.
78pub(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        // The first qualifying relationship wins even when its target is absent.
168        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}