1use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
4use quick_xml::{Reader, Writer};
5
6use crate::error::{OpcError, Result};
7
8pub mod rel_types {
10 pub const CORE_PROPERTIES: &str =
12 "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
13 pub const THUMBNAIL: &str =
14 "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail";
15
16 pub const DOCUMENT: &str =
18 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
19 pub const EXTENDED_PROPERTIES: &str =
20 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties";
21 pub const CUSTOM_PROPERTIES: &str =
22 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties";
23 pub const STYLES: &str =
24 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
25 pub const NUMBERING: &str =
26 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
27 pub const HEADER: &str =
28 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header";
29 pub const FOOTER: &str =
30 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer";
31 pub const IMAGE: &str =
32 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
33 pub const SETTINGS: &str =
34 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings";
35 pub const FONT_TABLE: &str =
36 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable";
37 pub const THEME: &str =
38 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";
39 pub const HYPERLINK: &str =
40 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
41 pub const FOOTNOTES: &str =
42 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes";
43 pub const ENDNOTES: &str =
44 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes";
45 pub const CHART: &str =
46 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart";
47 pub const PACKAGE: &str =
48 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/package";
49
50 pub const WORKSHEET: &str =
52 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet";
53 pub const SHARED_STRINGS: &str =
54 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings";
55
56 pub const SLIDE: &str =
58 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
59 pub const SLIDE_LAYOUT: &str =
60 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout";
61 pub const SLIDE_MASTER: &str =
62 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster";
63 pub const NOTES_SLIDE: &str =
64 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide";
65 pub const NOTES_MASTER: &str =
66 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster";
67 pub const PRES_PROPS: &str =
68 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps";
69 pub const VIEW_PROPS: &str =
70 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps";
71 pub const TABLE_STYLES: &str =
72 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles";
73 pub const HANDOUT_MASTER: &str =
74 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/handoutMaster";
75}
76
77#[derive(Debug, Clone, PartialEq)]
79pub struct Relationship {
80 pub id: String,
81 pub rel_type: String,
82 pub target: String,
83 pub target_mode: Option<String>,
84}
85
86#[derive(Debug, Clone, Default)]
88pub struct Relationships {
89 pub items: Vec<Relationship>,
90 next_id: u32,
91}
92
93impl Relationships {
94 pub fn new() -> Self {
95 Self {
96 items: Vec::new(),
97 next_id: 1,
98 }
99 }
100
101 pub fn from_xml(xml: &[u8]) -> Result<Self> {
103 let mut reader = Reader::from_reader(xml);
104 reader.config_mut().trim_text(true);
105
106 let mut items = Vec::new();
107 let mut max_id: u32 = 0;
108 let mut buf = Vec::new();
109
110 loop {
111 match reader.read_event_into(&mut buf) {
112 Ok(Event::Empty(ref e)) if e.name().as_ref() == b"Relationship" => {
113 let mut id = None;
114 let mut rel_type = None;
115 let mut target = None;
116 let mut target_mode = None;
117
118 for attr in e.attributes() {
119 let attr = attr?;
120 match attr.key.as_ref() {
121 b"Id" => {
122 let val = std::str::from_utf8(&attr.value)?.to_string();
123 if let Some(num_str) = val.strip_prefix("rId")
125 && let Ok(n) = num_str.parse::<u32>()
126 {
127 max_id = max_id.max(n);
128 }
129 id = Some(val);
130 }
131 b"Type" => {
132 rel_type = Some(std::str::from_utf8(&attr.value)?.to_string());
133 }
134 b"Target" => {
135 target = Some(std::str::from_utf8(&attr.value)?.to_string());
136 }
137 b"TargetMode" => {
138 target_mode = Some(std::str::from_utf8(&attr.value)?.to_string());
139 }
140 _ => {}
141 }
142 }
143
144 match (id, rel_type, target) {
145 (Some(id), Some(rel_type), Some(target)) => {
146 items.push(Relationship {
147 id,
148 rel_type,
149 target,
150 target_mode,
151 });
152 }
153 _ => return Err(OpcError::InvalidRelationship),
154 }
155 }
156 Ok(Event::Eof) => break,
157 Err(e) => return Err(e.into()),
158 _ => {}
159 }
160 buf.clear();
161 }
162
163 Ok(Relationships {
164 items,
165 next_id: next_relationship_number(max_id),
166 })
167 }
168
169 pub fn to_xml(&self) -> Result<Vec<u8>> {
171 let mut writer = Writer::new_with_indent(Vec::new(), b' ', 2);
172
173 writer.write_event(Event::Decl(BytesDecl::new(
174 "1.0",
175 Some("UTF-8"),
176 Some("yes"),
177 )))?;
178
179 let mut rels_start = BytesStart::new("Relationships");
180 rels_start.push_attribute((
181 "xmlns",
182 "http://schemas.openxmlformats.org/package/2006/relationships",
183 ));
184 writer.write_event(Event::Start(rels_start))?;
185
186 for rel in &self.items {
187 let mut elem = BytesStart::new("Relationship");
188 elem.push_attribute(("Id", rel.id.as_str()));
189 elem.push_attribute(("Type", rel.rel_type.as_str()));
190 elem.push_attribute(("Target", rel.target.as_str()));
191 if let Some(ref mode) = rel.target_mode {
192 elem.push_attribute(("TargetMode", mode.as_str()));
193 }
194 writer.write_event(Event::Empty(elem))?;
195 }
196
197 writer.write_event(Event::End(BytesEnd::new("Relationships")))?;
198
199 Ok(writer.into_inner())
200 }
201
202 pub fn get_by_id(&self, id: &str) -> Option<&Relationship> {
204 self.items.iter().find(|r| r.id == id)
205 }
206
207 pub fn get_by_type(&self, rel_type: &str) -> Option<&Relationship> {
209 self.items.iter().find(|r| r.rel_type == rel_type)
210 }
211
212 pub fn get_all_by_type(&self, rel_type: &str) -> Vec<&Relationship> {
214 self.items
215 .iter()
216 .filter(|r| r.rel_type == rel_type)
217 .collect()
218 }
219
220 pub fn add(&mut self, rel_type: &str, target: &str) -> String {
222 let id = self.allocate_id();
223 self.items.push(Relationship {
224 id: id.clone(),
225 rel_type: rel_type.to_string(),
226 target: target.to_string(),
227 target_mode: None,
228 });
229 id
230 }
231
232 pub fn add_external(&mut self, rel_type: &str, target: &str) -> String {
235 let id = self.allocate_id();
236 self.items.push(Relationship {
237 id: id.clone(),
238 rel_type: rel_type.to_string(),
239 target: target.to_string(),
240 target_mode: Some("External".to_string()),
241 });
242 id
243 }
244
245 pub fn add_with_id(&mut self, id: &str, rel_type: &str, target: &str) {
250 self.items.retain(|r| r.id != id);
251 self.items.push(Relationship {
252 id: id.to_string(),
253 rel_type: rel_type.to_string(),
254 target: target.to_string(),
255 target_mode: None,
256 });
257 if let Some(num) = id.strip_prefix("rId").and_then(|s| s.parse::<u32>().ok())
258 && num >= self.next_id
259 {
260 self.next_id = next_relationship_number(num);
261 }
262 }
263
264 fn allocate_id(&mut self) -> String {
265 let mut candidate = self.next_id;
266 let numeric_space = usize::try_from(u32::MAX).unwrap_or(usize::MAX);
267 let attempts = self.items.len().saturating_add(1).min(numeric_space);
268 for _ in 0..attempts {
269 let id = format!("rId{candidate}");
270 let next = next_relationship_number(candidate);
271 if self.items.iter().all(|relationship| relationship.id != id) {
272 self.next_id = next;
273 return id;
274 }
275 candidate = next;
276 }
277
278 for ordinal in 1u128.. {
279 let id = format!("rIdGenerated{ordinal}");
280 if self.items.iter().all(|relationship| relationship.id != id) {
281 return id;
282 }
283 }
284 unreachable!("the generated relationship id space is unbounded")
285 }
286}
287
288fn next_relationship_number(current: u32) -> u32 {
289 current.checked_add(1).unwrap_or(1).max(1)
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn relationship_and_content_type_constants_are_unique_and_well_formed() {
298 const PACKAGE_PREFIX: &str =
299 "http://schemas.openxmlformats.org/package/2006/relationships/";
300 const OFFICE_PREFIX: &str =
301 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/";
302
303 let package_relationships = [rel_types::CORE_PROPERTIES, rel_types::THUMBNAIL];
304 let office_relationships = [
305 rel_types::DOCUMENT,
306 rel_types::STYLES,
307 rel_types::NUMBERING,
308 rel_types::HEADER,
309 rel_types::FOOTER,
310 rel_types::IMAGE,
311 rel_types::SETTINGS,
312 rel_types::FONT_TABLE,
313 rel_types::THEME,
314 rel_types::HYPERLINK,
315 rel_types::FOOTNOTES,
316 rel_types::ENDNOTES,
317 rel_types::CHART,
318 rel_types::PACKAGE,
319 rel_types::WORKSHEET,
320 rel_types::SHARED_STRINGS,
321 rel_types::EXTENDED_PROPERTIES,
322 rel_types::CUSTOM_PROPERTIES,
323 rel_types::SLIDE,
324 rel_types::SLIDE_LAYOUT,
325 rel_types::SLIDE_MASTER,
326 rel_types::NOTES_SLIDE,
327 rel_types::NOTES_MASTER,
328 rel_types::PRES_PROPS,
329 rel_types::VIEW_PROPS,
330 rel_types::TABLE_STYLES,
331 rel_types::HANDOUT_MASTER,
332 ];
333
334 let mut relationship_values = std::collections::HashSet::new();
335 for value in package_relationships {
336 assert!(value.starts_with(PACKAGE_PREFIX));
337 assert!(!value.chars().any(char::is_whitespace));
338 assert!(relationship_values.insert(value));
339 }
340 for value in office_relationships {
341 assert!(value.starts_with(OFFICE_PREFIX));
342 assert!(!value.chars().any(char::is_whitespace));
343 assert!(relationship_values.insert(value));
344 }
345
346 let content_type_values = [
347 crate::content_types::RELATIONSHIPS,
348 crate::content_types::XML,
349 crate::content_types::CORE_PROPERTIES,
350 crate::content_types::EXTENDED_PROPERTIES,
351 crate::content_types::CUSTOM_PROPERTIES,
352 crate::content_types::THEME,
353 crate::content_types::CHART,
354 crate::content_types::PRESENTATION,
355 crate::content_types::SLIDESHOW,
356 crate::content_types::SLIDE,
357 crate::content_types::SLIDE_LAYOUT,
358 crate::content_types::SLIDE_MASTER,
359 crate::content_types::NOTES_SLIDE,
360 crate::content_types::NOTES_MASTER,
361 crate::content_types::PRES_PROPS,
362 crate::content_types::VIEW_PROPS,
363 crate::content_types::TABLE_STYLES,
364 crate::content_types::HANDOUT_MASTER,
365 crate::content_types::WORKBOOK,
366 crate::content_types::EMBEDDED_WORKBOOK,
367 crate::content_types::WORKSHEET,
368 crate::content_types::SHARED_STRINGS,
369 crate::content_types::STYLES,
370 ];
371
372 let mut content_types = std::collections::HashSet::new();
373 for value in content_type_values {
374 let (kind, subtype) = value.split_once('/').expect("valid MIME type");
375 assert_eq!(kind, "application");
376 assert!(!subtype.is_empty());
377 assert!(!subtype.contains('/'));
378 assert!(!value.chars().any(char::is_whitespace));
379 assert!(content_types.insert(value));
380 }
381 }
382
383 #[test]
384 fn round_trip_relationships() {
385 let mut rels = Relationships::new();
386 rels.add(rel_types::DOCUMENT, "word/document.xml");
387 rels.add(rel_types::STYLES, "word/styles.xml");
388
389 let xml = rels.to_xml().unwrap();
390 let parsed = Relationships::from_xml(&xml).unwrap();
391
392 assert_eq!(parsed.items.len(), 2);
393 assert_eq!(parsed.items[0].id, "rId1");
394 assert_eq!(parsed.items[0].target, "word/document.xml");
395 assert_eq!(parsed.items[1].id, "rId2");
396 }
397
398 #[test]
399 fn find_by_type() {
400 let mut rels = Relationships::new();
401 rels.add(rel_types::DOCUMENT, "word/document.xml");
402 rels.add(rel_types::STYLES, "word/styles.xml");
403
404 let doc = rels.get_by_type(rel_types::DOCUMENT).unwrap();
405 assert_eq!(doc.target, "word/document.xml");
406 }
407
408 #[test]
409 fn high_numeric_relationship_ids_roll_over_without_collision() {
410 let xml = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
411<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
412 <Relationship Id="rId4294967294" Type="type-a" Target="a.xml"/>
413</Relationships>"#;
414 let mut relationships = Relationships::from_xml(xml).unwrap();
415 assert_eq!(relationships.add("type-b", "b.xml"), "rId4294967295");
416 assert_eq!(relationships.add("type-c", "c.xml"), "rId1");
417 assert_eq!(
418 relationships.add_external("type-d", "https://example.com"),
419 "rId2"
420 );
421 let ids = relationships
422 .items
423 .iter()
424 .map(|relationship| relationship.id.as_str())
425 .collect::<std::collections::HashSet<_>>();
426 assert_eq!(ids.len(), relationships.items.len());
427 assert!(!ids.contains("rId0"));
428 }
429
430 #[test]
431 fn parsed_u32_max_relationship_id_rolls_to_first_free_positive_id() {
432 let xml = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
433<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
434 <Relationship Id="rId4294967295" Type="type-max" Target="max.xml"/>
435 <Relationship Id="rId1" Type="type-one" Target="one.xml"/>
436</Relationships>"#;
437 let mut relationships = Relationships::from_xml(xml).unwrap();
438 assert_eq!(relationships.add("type-two", "two.xml"), "rId2");
439 relationships.add_with_id("rId4294967295", "type-max", "replacement.xml");
440 assert_eq!(relationships.add("type-three", "three.xml"), "rId3");
441 }
442}