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