1use crate::hash::FastMap;
9use crate::xml::{unescape_attr, Event, Ns, Reader, XmlError};
10
11#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct Defect {
14 pub offset: usize,
15 pub msg: &'static str,
16}
17
18pub const RELATIONSHIPS_CONTENT_TYPE: &str =
20 "application/vnd.openxmlformats-package.relationships+xml";
21pub const CORE_PROPERTIES_CONTENT_TYPE: &str =
23 "application/vnd.openxmlformats-package.core-properties+xml";
24pub const CORE_PROPERTIES_REL: &str =
26 "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
27pub const THUMBNAIL_REL: &str =
29 "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail";
30pub const CONTENT_TYPES_ITEM: &str = "[Content_Types].xml";
32pub const PACKAGE_RELS: &str = "/_rels/.rels";
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum PartNameError {
38 Empty,
39 NoLeadingSlash,
40 EmptySegment,
41 TrailingSlash,
42 SegmentEndsWithDot,
43 ForbiddenCharacter(u8),
44 BadPercentEncoding,
45 PercentEncodedSlash,
46 PercentEncodedUnreserved,
47}
48
49impl std::fmt::Display for PartNameError {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 match self {
52 PartNameError::Empty => f.write_str("empty part name"),
53 PartNameError::NoLeadingSlash => f.write_str("part name does not start with '/'"),
54 PartNameError::EmptySegment => f.write_str("empty segment"),
55 PartNameError::TrailingSlash => f.write_str("trailing '/'"),
56 PartNameError::SegmentEndsWithDot => f.write_str("segment ends with '.'"),
57 PartNameError::ForbiddenCharacter(byte) => write!(
58 f,
59 "character {:?} must be percent-encoded",
60 char::from(*byte)
61 ),
62 PartNameError::BadPercentEncoding => f.write_str("'%' not followed by two hex digits"),
63 PartNameError::PercentEncodedSlash => f.write_str("percent-encoded slash"),
64 PartNameError::PercentEncodedUnreserved => {
65 f.write_str("percent-encoded unreserved character")
66 }
67 }
68 }
69}
70
71pub fn validate_part_name(name: &str) -> Result<(), PartNameError> {
73 if name.is_empty() {
74 return Err(PartNameError::Empty);
75 }
76 if !name.starts_with('/') {
77 return Err(PartNameError::NoLeadingSlash);
78 }
79 if name.ends_with('/') {
80 return Err(PartNameError::TrailingSlash);
81 }
82 for segment in name[1..].split('/') {
83 if segment.is_empty() {
84 return Err(PartNameError::EmptySegment);
85 }
86 if segment.ends_with('.') {
87 return Err(PartNameError::SegmentEndsWithDot);
88 }
89 let bytes = segment.as_bytes();
90 let mut i = 0;
91 while i < bytes.len() {
92 let byte = bytes[i];
93 if byte == b'%' {
94 let hex = bytes
95 .get(i + 1..i + 3)
96 .filter(|hex| hex.iter().all(u8::is_ascii_hexdigit))
97 .ok_or(PartNameError::BadPercentEncoding)?;
98 let value =
99 u8::from_str_radix(std::str::from_utf8(hex).unwrap_or("00"), 16).unwrap_or(0);
100 if value == b'/' || value == b'\\' {
101 return Err(PartNameError::PercentEncodedSlash);
102 }
103 if value.is_ascii_alphanumeric() || matches!(value, b'-' | b'.' | b'_' | b'~') {
104 return Err(PartNameError::PercentEncodedUnreserved);
105 }
106 i += 3;
107 continue;
108 }
109 if byte >= 0x80
110 || byte.is_ascii_alphanumeric()
111 || matches!(
112 byte,
113 b'-' | b'.'
114 | b'_'
115 | b'~'
116 | b'!'
117 | b'$'
118 | b'&'
119 | b'\''
120 | b'('
121 | b')'
122 | b'*'
123 | b'+'
124 | b','
125 | b';'
126 | b'='
127 | b':'
128 | b'@'
129 )
130 {
131 i += 1;
132 continue;
133 }
134 return Err(PartNameError::ForbiddenCharacter(byte));
135 }
136 }
137 Ok(())
138}
139
140pub fn equivalence_key(name: &str) -> String {
142 name.to_ascii_lowercase()
143}
144
145pub fn is_derivable(name: &str, other: &str) -> bool {
147 let name = equivalence_key(name);
148 let mut prefix = equivalence_key(other);
149 prefix.push('/');
150 name.starts_with(&prefix)
151}
152
153pub fn rels_part_name(source: &str) -> String {
155 if source == "/" || source.is_empty() {
156 return PACKAGE_RELS.to_string();
157 }
158 let (dir, base) = match source.rfind('/') {
159 Some(slash) => (&source[..slash], &source[slash + 1..]),
160 None => ("", source),
161 };
162 format!("{dir}/_rels/{base}.rels")
163}
164
165pub fn source_of_rels(rels: &str) -> Option<String> {
167 let stem = rels.strip_suffix(".rels")?;
168 let slash = stem.rfind('/')?;
169 let (dir, base) = (&stem[..slash], &stem[slash + 1..]);
170 let dir = dir.strip_suffix("/_rels")?;
171 if base.is_empty() {
172 return match dir.is_empty() {
173 true => Some("/".to_string()),
174 false => None,
175 };
176 }
177 Some(format!("{dir}/{base}"))
178}
179
180pub fn is_rels_part(name: &str) -> bool {
182 source_of_rels(name).is_some()
183}
184
185pub fn extension(name: &str) -> Option<&str> {
187 let last = name.rsplit('/').next()?;
188 let dot = last.rfind('.')?;
189 Some(&last[dot + 1..])
190}
191
192pub fn resolve_target(base: &str, target: &str) -> String {
197 let target = target.split(['?', '#']).next().unwrap_or("");
198 let target = target.replace('\\', "/");
199 let merged = match target.starts_with('/') {
200 true => target,
201 false => {
202 let dir_end = base.rfind('/').map_or(0, |slash| slash + 1);
203 format!("{}{}", &base[..dir_end], target)
204 }
205 };
206 remove_dot_segments(&merged)
207}
208
209fn remove_dot_segments(path: &str) -> String {
210 let mut out: Vec<&str> = Vec::new();
211 for segment in path.split('/').skip(1) {
212 match segment {
213 "." | "" => {}
214 ".." => {
215 out.pop();
216 }
217 other => out.push(other),
218 }
219 }
220 let mut result = String::with_capacity(path.len());
221 for segment in &out {
222 result.push('/');
223 result.push_str(segment);
224 }
225 if result.is_empty() {
226 result.push('/');
227 }
228 result
229}
230
231#[derive(Clone, Debug, Default)]
233pub struct ContentTypes {
234 defaults: Vec<(String, String)>,
235 default_index: FastMap<String, usize>,
236 overrides: Vec<(String, String)>,
237 override_index: FastMap<String, usize>,
238 pub duplicate_defaults: Vec<usize>,
240 pub duplicate_overrides: Vec<usize>,
242 pub defects: Vec<Defect>,
243 pub wrong_namespace: bool,
245}
246
247impl ContentTypes {
248 pub fn parse(xml: &[u8]) -> Result<Self, XmlError> {
250 let mut reader = Reader::new(xml);
251 let mut types = ContentTypes::default();
252 loop {
253 match reader.next()? {
254 Event::Start(start) if reader.depth() == 1 => {
255 types.wrong_namespace =
256 start.name.ns != Ns::ContentTypes || start.name.local != b"Types";
257 }
258 Event::Start(start) if reader.depth() == 2 && start.name.local == b"Default" => {
259 let ext = reader
260 .attr(&start, Ns::None, b"Extension")
261 .map(unescape_attr);
262 let content_type = reader
263 .attr(&start, Ns::None, b"ContentType")
264 .map(unescape_attr);
265 match (ext, content_type) {
266 (Some(ext), Some(content_type)) => types.add_default(ext, content_type),
267 _ => types.defects.push(Defect {
268 offset: start.offset,
269 msg: "Default element without Extension or ContentType",
270 }),
271 }
272 reader.skip_element()?;
273 }
274 Event::Start(start) if reader.depth() == 2 && start.name.local == b"Override" => {
275 let part_name = reader
276 .attr(&start, Ns::None, b"PartName")
277 .map(unescape_attr);
278 let content_type = reader
279 .attr(&start, Ns::None, b"ContentType")
280 .map(unescape_attr);
281 match (part_name, content_type) {
282 (Some(part_name), Some(content_type)) => {
283 types.add_override(part_name, content_type)
284 }
285 _ => types.defects.push(Defect {
286 offset: start.offset,
287 msg: "Override element without PartName or ContentType",
288 }),
289 }
290 reader.skip_element()?;
291 }
292 Event::Start(start) => {
293 types.defects.push(Defect {
294 offset: start.offset,
295 msg: "unexpected element in the content types stream",
296 });
297 reader.skip_element()?;
298 }
299 Event::Eof => return Ok(types),
300 _ => {}
301 }
302 }
303 }
304
305 fn add_default(&mut self, extension: String, content_type: String) {
306 let key = extension.to_ascii_lowercase();
307 let index = self.defaults.len();
308 self.defaults.push((extension, content_type));
309 if self.default_index.contains_key(&key) {
310 self.duplicate_defaults.push(index);
311 return;
312 }
313 self.default_index.insert(key, index);
314 }
315
316 fn add_override(&mut self, part_name: String, content_type: String) {
317 let key = equivalence_key(&part_name);
318 let index = self.overrides.len();
319 self.overrides.push((part_name, content_type));
320 if self.override_index.contains_key(&key) {
321 self.duplicate_overrides.push(index);
322 return;
323 }
324 self.override_index.insert(key, index);
325 }
326
327 pub fn defaults(&self) -> &[(String, String)] {
329 &self.defaults
330 }
331
332 pub fn overrides(&self) -> &[(String, String)] {
334 &self.overrides
335 }
336
337 pub fn content_type_of(&self, part_name: &str) -> Option<&str> {
340 if let Some(&index) = self.override_index.get(&equivalence_key(part_name)) {
341 return Some(&self.overrides[index].1);
342 }
343 let ext = extension(part_name)?.to_ascii_lowercase();
344 self.default_index
345 .get(&ext)
346 .map(|&index| self.defaults[index].1.as_str())
347 }
348
349 pub fn default_for(&self, extension: &str) -> Option<&str> {
351 self.default_index
352 .get(&extension.to_ascii_lowercase())
353 .map(|&index| self.defaults[index].1.as_str())
354 }
355
356 pub fn override_for(&self, part_name: &str) -> Option<&str> {
358 self.override_index
359 .get(&equivalence_key(part_name))
360 .map(|&index| self.overrides[index].1.as_str())
361 }
362}
363
364#[derive(Clone, Copy, Debug, PartialEq, Eq)]
366pub enum TargetMode {
367 Internal,
368 External,
369}
370
371#[derive(Clone, Debug, PartialEq, Eq)]
373pub struct Relationship {
374 pub id: String,
375 pub rel_type: String,
376 pub target: String,
377 pub mode: TargetMode,
378 pub offset: usize,
380}
381
382#[derive(Clone, Debug, Default)]
384pub struct Relationships {
385 pub source: String,
387 items: Vec<Relationship>,
388 index: FastMap<String, usize>,
389 pub duplicate_ids: Vec<usize>,
391 pub defects: Vec<Defect>,
392 pub wrong_namespace: bool,
393}
394
395impl Relationships {
396 pub fn empty(source: &str) -> Self {
398 Self {
399 source: source.to_string(),
400 ..Self::default()
401 }
402 }
403
404 pub fn parse(source: &str, xml: &[u8]) -> Result<Self, XmlError> {
406 let mut reader = Reader::new(xml);
407 let mut rels = Relationships::empty(source);
408 loop {
409 match reader.next()? {
410 Event::Start(start) if reader.depth() == 1 => {
411 rels.wrong_namespace =
412 start.name.ns != Ns::PkgRel || start.name.local != b"Relationships";
413 }
414 Event::Start(start)
415 if reader.depth() == 2 && start.name.local == b"Relationship" =>
416 {
417 let mut id = None;
418 let mut rel_type = None;
419 let mut target = None;
420 let mut mode = TargetMode::Internal;
421 for attr in reader.attrs(&start) {
422 if attr.name.ns != Ns::None {
423 continue;
424 }
425 match attr.name.local {
426 b"Id" => id = Some(unescape_attr(attr.raw_value)),
427 b"Type" => rel_type = Some(unescape_attr(attr.raw_value)),
428 b"Target" => target = Some(unescape_attr(attr.raw_value)),
429 b"TargetMode" => match attr.raw_value {
430 b"External" => mode = TargetMode::External,
431 b"Internal" => mode = TargetMode::Internal,
432 _ => rels.defects.push(Defect {
433 offset: start.offset,
434 msg: "TargetMode is neither Internal nor External",
435 }),
436 },
437 _ => {}
438 }
439 }
440 match (id, rel_type, target) {
441 (Some(id), Some(rel_type), Some(target)) => rels.add(Relationship {
442 id,
443 rel_type,
444 target,
445 mode,
446 offset: start.offset,
447 }),
448 _ => rels.defects.push(Defect {
449 offset: start.offset,
450 msg: "Relationship without Id, Type or Target",
451 }),
452 }
453 reader.skip_element()?;
454 }
455 Event::Start(start) => {
456 rels.defects.push(Defect {
457 offset: start.offset,
458 msg: "unexpected element in a Relationships part",
459 });
460 reader.skip_element()?;
461 }
462 Event::Eof => return Ok(rels),
463 _ => {}
464 }
465 }
466 }
467
468 fn add(&mut self, rel: Relationship) {
469 let index = self.items.len();
470 if self.index.contains_key(&rel.id) {
471 self.duplicate_ids.push(index);
472 self.items.push(rel);
473 return;
474 }
475 self.index.insert(rel.id.clone(), index);
476 self.items.push(rel);
477 }
478
479 pub fn iter(&self) -> impl Iterator<Item = &Relationship> {
480 self.items.iter()
481 }
482
483 pub fn len(&self) -> usize {
484 self.items.len()
485 }
486
487 pub fn is_empty(&self) -> bool {
488 self.items.is_empty()
489 }
490
491 pub fn get(&self, id: &str) -> Option<&Relationship> {
493 self.index.get(id).map(|&index| &self.items[index])
494 }
495
496 pub fn by_type<'s>(&'s self, rel_type: &'s str) -> impl Iterator<Item = &'s Relationship> + 's {
498 self.items
499 .iter()
500 .filter(move |rel| rel.rel_type == rel_type)
501 }
502
503 pub fn first_of_type(&self, rel_type: &str) -> Option<&Relationship> {
504 self.items.iter().find(|rel| rel.rel_type == rel_type)
505 }
506
507 pub fn resolve(&self, rel: &Relationship) -> Option<String> {
509 match rel.mode {
510 TargetMode::External => None,
511 TargetMode::Internal => Some(resolve_target(&self.source, &rel.target)),
512 }
513 }
514
515 pub fn target_of(&self, id: &str) -> Option<String> {
517 self.get(id).and_then(|rel| self.resolve(rel))
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524
525 #[test]
526 fn part_name_grammar() {
527 assert_eq!(validate_part_name("/ppt/slides/slide1.xml"), Ok(()));
528 assert_eq!(validate_part_name("/a/%20b/c.xml"), Ok(()));
529 assert_eq!(validate_part_name("/ppt/média.xml"), Ok(()));
530 assert_eq!(validate_part_name(""), Err(PartNameError::Empty));
531 assert_eq!(
532 validate_part_name("ppt/x.xml"),
533 Err(PartNameError::NoLeadingSlash)
534 );
535 assert_eq!(
536 validate_part_name("/ppt/"),
537 Err(PartNameError::TrailingSlash)
538 );
539 assert_eq!(
540 validate_part_name("/ppt//x.xml"),
541 Err(PartNameError::EmptySegment)
542 );
543 assert_eq!(
544 validate_part_name("/ppt/./x.xml"),
545 Err(PartNameError::SegmentEndsWithDot)
546 );
547 assert_eq!(
548 validate_part_name("/ppt/../x.xml"),
549 Err(PartNameError::SegmentEndsWithDot)
550 );
551 assert_eq!(
552 validate_part_name("/ppt/x."),
553 Err(PartNameError::SegmentEndsWithDot)
554 );
555 assert_eq!(
556 validate_part_name("/a b.xml"),
557 Err(PartNameError::ForbiddenCharacter(b' '))
558 );
559 assert_eq!(
560 validate_part_name("/a%2Fb.xml"),
561 Err(PartNameError::PercentEncodedSlash)
562 );
563 assert_eq!(
564 validate_part_name("/%41.xml"),
565 Err(PartNameError::PercentEncodedUnreserved)
566 );
567 assert_eq!(
568 validate_part_name("/%XY.xml"),
569 Err(PartNameError::BadPercentEncoding)
570 );
571 assert_eq!(
572 validate_part_name("/[Content_Types].xml"),
573 Err(PartNameError::ForbiddenCharacter(b'['))
574 );
575 }
576
577 #[test]
578 fn equivalence_and_derivability_are_segment_aware() {
579 assert_eq!(
580 equivalence_key("/PPT/Slides/Slide1.XML"),
581 "/ppt/slides/slide1.xml"
582 );
583 assert!(is_derivable("/a/b", "/a"));
584 assert!(is_derivable("/A/b", "/a"));
585 assert!(!is_derivable("/ab", "/a"));
586 assert!(!is_derivable("/a", "/a"));
587 }
588
589 #[test]
590 fn rels_names_round_trip() {
591 assert_eq!(rels_part_name("/"), "/_rels/.rels");
592 assert_eq!(
593 rels_part_name("/ppt/presentation.xml"),
594 "/ppt/_rels/presentation.xml.rels"
595 );
596 assert_eq!(
597 rels_part_name("/ppt/slides/slide1.xml"),
598 "/ppt/slides/_rels/slide1.xml.rels"
599 );
600 assert_eq!(source_of_rels("/_rels/.rels").as_deref(), Some("/"));
601 assert_eq!(
602 source_of_rels("/ppt/slides/_rels/slide1.xml.rels").as_deref(),
603 Some("/ppt/slides/slide1.xml")
604 );
605 assert_eq!(source_of_rels("/ppt/slides/slide1.xml"), None);
606 assert_eq!(source_of_rels("/ppt/_rels/.rels"), None);
607 assert!(is_rels_part("/_rels/.rels"));
608 assert!(!is_rels_part("/ppt/slides/slide1.xml"));
609 }
610
611 #[test]
612 fn extensions_come_from_the_last_segment_only() {
613 assert_eq!(extension("/ppt/slides/slide1.xml"), Some("xml"));
614 assert_eq!(extension("/ppt.d/media/image"), None);
615 assert_eq!(extension("/ppt/media/image1.PNG"), Some("PNG"));
616 assert_eq!(extension("/_rels/.rels"), Some("rels"));
617 }
618
619 #[test]
620 fn targets_resolve_against_the_source_part() {
621 assert_eq!(
622 resolve_target("/ppt/slides/slide1.xml", "../slideLayouts/slideLayout1.xml"),
623 "/ppt/slideLayouts/slideLayout1.xml"
624 );
625 assert_eq!(
626 resolve_target("/", "ppt/presentation.xml"),
627 "/ppt/presentation.xml"
628 );
629 assert_eq!(
630 resolve_target("/", "/ppt/presentation.xml"),
631 "/ppt/presentation.xml"
632 );
633 assert_eq!(resolve_target("/a/b/foo.xml", "bar.xml"), "/a/b/bar.xml");
634 assert_eq!(resolve_target("/a/b/foo.xml", "./bar.xml"), "/a/b/bar.xml");
635 assert_eq!(resolve_target("/a/b/foo.xml", "/b/bar.xml"), "/b/bar.xml");
636 assert_eq!(resolve_target("/", "../bar.xml"), "/bar.xml");
637 assert_eq!(
638 resolve_target("/ppt/slides/slide1.xml", "../media/image1.png?x=1#frag"),
639 "/ppt/media/image1.png"
640 );
641 assert_eq!(
642 resolve_target("/ppt/slides/slide1.xml", "..\\media\\image1.png"),
643 "/ppt/media/image1.png"
644 );
645 assert_eq!(
646 resolve_target("/ppt/slides/slide1.xml", "../../../../x.xml"),
647 "/x.xml"
648 );
649 }
650
651 const TYPES: &[u8] = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
652<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
653<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
654<Default Extension="xml" ContentType="application/xml"/>
655<Default Extension="PNG" ContentType="image/png"/>
656<Default Extension="xml" ContentType="text/xml"/>
657<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
658<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
659<Override PartName="/PPT/slides/slide1.xml" ContentType="dup"/>
660<Bogus/>
661</Types>"#;
662
663 #[test]
664 fn content_types_match_overrides_then_defaults_case_insensitively() {
665 let types = ContentTypes::parse(TYPES).unwrap();
666 assert!(!types.wrong_namespace);
667 assert_eq!(types.content_type_of("/ppt/presentation.xml"), Some("application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"));
668 assert_eq!(
669 types.content_type_of("/ppt/Slides/Slide1.xml"),
670 Some("application/vnd.openxmlformats-officedocument.presentationml.slide+xml")
671 );
672 assert_eq!(
673 types.content_type_of("/ppt/slides/slide2.xml"),
674 Some("application/xml")
675 );
676 assert_eq!(
677 types.content_type_of("/ppt/media/image1.png"),
678 Some("image/png")
679 );
680 assert_eq!(types.content_type_of("/ppt/media/image1"), None);
681 assert_eq!(
682 types.content_type_of("/_rels/.rels"),
683 Some("application/vnd.openxmlformats-package.relationships+xml")
684 );
685 assert_eq!(types.defaults().len(), 4);
686 assert_eq!(types.duplicate_defaults, vec![3]);
687 assert_eq!(types.overrides().len(), 3);
688 assert_eq!(types.duplicate_overrides, vec![2]);
689 assert_eq!(types.defects.len(), 1);
690 assert_eq!(
691 types.defects[0].msg,
692 "unexpected element in the content types stream"
693 );
694 assert_eq!(
695 types.default_for("Rels"),
696 Some("application/vnd.openxmlformats-package.relationships+xml")
697 );
698 assert_eq!(
699 types.override_for("/ppt/slides/slide1.xml"),
700 Some("application/vnd.openxmlformats-officedocument.presentationml.slide+xml")
701 );
702 }
703
704 #[test]
705 fn content_types_in_the_wrong_namespace_are_flagged() {
706 let types = ContentTypes::parse(
707 b"<Types><Default Extension=\"xml\" ContentType=\"application/xml\"/></Types>",
708 )
709 .unwrap();
710 assert!(types.wrong_namespace);
711 assert_eq!(types.content_type_of("/a.xml"), Some("application/xml"));
712 assert!(ContentTypes::parse(b"<Types").is_err());
713 }
714
715 const RELS: &[u8] = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
716<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
717<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
718<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="https://example.com/?a=1&b=2" TargetMode="External"/>
719<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image1.png"/>
720<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image2.png"/>
721<Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="x" TargetMode="Sideways"/>
722<Relationship Id="rId6" Target="nothing"/>
723</Relationships>"#;
724
725 #[test]
726 fn relationships_keep_duplicates_and_resolve_internal_targets() {
727 let rels = Relationships::parse("/ppt/slides/slide1.xml", RELS).unwrap();
728 assert!(!rels.wrong_namespace);
729 assert_eq!(rels.len(), 5);
730 assert_eq!(rels.duplicate_ids, vec![3]);
731 assert_eq!(rels.defects.len(), 2);
732 assert_eq!(
733 rels.target_of("rId1").as_deref(),
734 Some("/ppt/slideLayouts/slideLayout1.xml")
735 );
736 let link = rels.get("rId2").unwrap();
737 assert_eq!(link.mode, TargetMode::External);
738 assert_eq!(link.target, "https://example.com/?a=1&b=2");
739 assert_eq!(rels.resolve(link), None);
740 assert_eq!(
741 rels.target_of("rId3").as_deref(),
742 Some("/ppt/media/image1.png")
743 );
744 assert_eq!(
745 rels.by_type(
746 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
747 )
748 .count(),
749 3
750 );
751 assert!(rels.first_of_type("urn:none").is_none());
752 assert_eq!(rels.get("rId6"), None);
753 let package = Relationships::parse("/", b"<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"><Relationship Id=\"a\" Type=\"t\" Target=\"ppt/presentation.xml\"/></Relationships>").unwrap();
754 assert_eq!(
755 package.target_of("a").as_deref(),
756 Some("/ppt/presentation.xml")
757 );
758 }
759}