pptx_parse/
relationships.rs1use 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}