Skip to main content

pptx_parse/
relationships.rs

1use serde::{Deserialize, Serialize};
2
3use crate::xml::{ParseBudget, parse_xml};
4use crate::{PptxError, xml::XmlElement};
5
6pub mod relationship_types {
7    pub const SLIDE: &str = "/slide";
8    pub const SLIDE_LAYOUT: &str = "/slideLayout";
9    pub const SLIDE_MASTER: &str = "/slideMaster";
10    pub const THEME: &str = "/theme";
11    pub const IMAGE: &str = "/image";
12    pub const CHART: &str = "/chart";
13}
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub enum TargetMode {
18    #[default]
19    Internal,
20    External,
21}
22
23#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct Relationship {
26    pub id: String,
27    pub relationship_type: String,
28    pub target: String,
29    pub target_mode: TargetMode,
30    pub resolved_target: Option<String>,
31}
32
33impl Relationship {
34    pub fn has_type(&self, suffix: &str) -> bool {
35        self.relationship_type.ends_with(suffix)
36    }
37}
38
39pub(crate) fn parse_relationships(
40    xml: &[u8],
41    relationship_part: &str,
42    source: &str,
43    budget: &mut ParseBudget<'_>,
44) -> Result<Vec<Relationship>, PptxError> {
45    let root = parse_xml(xml, relationship_part, budget)?;
46    let mut relationships = Vec::new();
47    for element in root.children_named("Relationship") {
48        budget.charge_relationship(relationship_part)?;
49        relationships.push(parse_relationship(element, source)?);
50    }
51    Ok(relationships)
52}
53
54fn parse_relationship(element: &XmlElement, source: &str) -> Result<Relationship, PptxError> {
55    let id = element.attribute("Id").unwrap_or_default().to_owned();
56    let relationship_type = element.attribute("Type").unwrap_or_default().to_owned();
57    let target = element.attribute("Target").unwrap_or_default().to_owned();
58    let target_mode = if element.attribute("TargetMode") == Some("External") {
59        TargetMode::External
60    } else {
61        TargetMode::Internal
62    };
63    let resolved_target = match target_mode {
64        TargetMode::External => None,
65        TargetMode::Internal => Some(resolve_target(source, &target)?),
66    };
67    Ok(Relationship {
68        id,
69        relationship_type,
70        target,
71        target_mode,
72        resolved_target,
73    })
74}
75
76pub(crate) fn resolve_target(source: &str, target: &str) -> Result<String, PptxError> {
77    if target.contains("://") || target.starts_with("mailto:") || target.starts_with("data:") {
78        return Err(PptxError::InvalidRelationship {
79            source_part: source.to_owned(),
80            target: target.to_owned(),
81        });
82    }
83    let target = target
84        .split('#')
85        .next()
86        .unwrap_or_default()
87        .replace('\\', "/");
88    let mut segments = Vec::new();
89    if !target.starts_with('/')
90        && let Some((directory, _)) = source.rsplit_once('/')
91    {
92        segments.extend(directory.split('/').filter(|segment| !segment.is_empty()));
93    }
94    for segment in target.trim_start_matches('/').split('/') {
95        match segment {
96            "" | "." => {}
97            ".." => {
98                if segments.pop().is_none() {
99                    return Err(PptxError::InvalidRelationship {
100                        source_part: source.to_owned(),
101                        target: target.to_owned(),
102                    });
103                }
104            }
105            segment => segments.push(segment),
106        }
107    }
108    if segments.is_empty() {
109        return Err(PptxError::InvalidRelationship {
110            source_part: source.to_owned(),
111            target: target.to_owned(),
112        });
113    }
114    Ok(segments.join("/"))
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::ParseLimits;
121
122    #[test]
123    fn resolves_internal_targets_without_fetching_external_targets() {
124        assert_eq!(
125            resolve_target("ppt/slides/slide1.xml", "../slideLayouts/slideLayout1.xml").unwrap(),
126            "ppt/slideLayouts/slideLayout1.xml"
127        );
128        assert!(resolve_target("ppt/presentation.xml", "../../escape.xml").is_err());
129
130        let limits = ParseLimits::default();
131        let mut budget = ParseBudget::new(&limits);
132        let relationships = parse_relationships(
133            br#"<Relationships><Relationship Id="rId1" Type="hyperlink" Target="https://example.invalid" TargetMode="External"/></Relationships>"#,
134            "ppt/slides/_rels/slide1.xml.rels",
135            "ppt/slides/slide1.xml",
136            &mut budget,
137        )
138        .unwrap();
139        assert_eq!(relationships[0].target_mode, TargetMode::External);
140        assert_eq!(relationships[0].resolved_target, None);
141    }
142}