Skip to main content

pptxboss_core/
opc.rs

1//! Open Packaging Conventions (ECMA-376 Part 2): part names, the content
2//! types stream and relationships.
3//!
4//! Everything here is the raw view. Parsers keep duplicates and record
5//! defects instead of discarding them, so the verifier can report what a
6//! lenient reader would silently resolve.
7
8use crate::hash::FastMap;
9use crate::xml::{unescape_attr, Event, Ns, Reader, XmlError};
10
11/// A problem found while parsing, kept beside the result.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct Defect {
14    pub offset: usize,
15    pub msg: &'static str,
16}
17
18/// Media type of a Relationships part (Annex E).
19pub const RELATIONSHIPS_CONTENT_TYPE: &str =
20    "application/vnd.openxmlformats-package.relationships+xml";
21/// Media type of the Core Properties part (Annex E).
22pub const CORE_PROPERTIES_CONTENT_TYPE: &str =
23    "application/vnd.openxmlformats-package.core-properties+xml";
24/// Relationship type of the Core Properties part (Annex E).
25pub const CORE_PROPERTIES_REL: &str =
26    "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
27/// Relationship type of a thumbnail (Annex E).
28pub const THUMBNAIL_REL: &str =
29    "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail";
30/// The name of the content types stream in a ZIP package (7.3.7).
31pub const CONTENT_TYPES_ITEM: &str = "[Content_Types].xml";
32/// The package relationships part (6.5.2.2).
33pub const PACKAGE_RELS: &str = "/_rels/.rels";
34
35/// Why a string is not a part name (6.2.2.2).
36#[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
71/// Checks `name` against the part name grammar of 6.2.2.2.
72pub 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
140/// The equivalence key of a part name: ASCII letters folded to lower case (6.2.2.3).
141pub fn equivalence_key(name: &str) -> String {
142    name.to_ascii_lowercase()
143}
144
145/// True when `name` is `other` followed by `/` and more segments (6.2.2.3 "derivable").
146pub 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
153/// The Relationships part name for `source` (6.5.2.2, 6.5.2.3); `/` is the package.
154pub 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
165/// The source part of a Relationships part name, or `/` for the package rels.
166pub 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
180/// Whether `name` names a Relationships part.
181pub fn is_rels_part(name: &str) -> bool {
182    source_of_rels(name).is_some()
183}
184
185/// The extension of a part name: the text after the last `.` of its last segment (7.2.3.5).
186pub fn extension(name: &str) -> Option<&str> {
187    let last = name.rsplit('/').next()?;
188    let dot = last.rfind('.')?;
189    Some(&last[dot + 1..])
190}
191
192/// Resolves a relationship `target` against the part it was found in,
193/// returning an absolute part name (RFC 3986 5.2 on the path; 6.4).
194/// `base` is the source part for a part Relationships part and `/` for
195/// the package. Query and fragment are dropped.
196pub 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/// The content types stream (7.2.3), as declared.
232#[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    /// Indexes into `defaults()` whose extension repeats an earlier one.
239    pub duplicate_defaults: Vec<usize>,
240    /// Indexes into `overrides()` whose part name repeats an earlier one.
241    pub duplicate_overrides: Vec<usize>,
242    pub defects: Vec<Defect>,
243    /// True when the root was in a namespace other than the content types namespace.
244    pub wrong_namespace: bool,
245}
246
247impl ContentTypes {
248    /// Parses `[Content_Types].xml`; unknown elements are skipped and recorded.
249    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    /// `(Extension, ContentType)` pairs in document order.
328    pub fn defaults(&self) -> &[(String, String)] {
329        &self.defaults
330    }
331
332    /// `(PartName, ContentType)` pairs in document order.
333    pub fn overrides(&self) -> &[(String, String)] {
334        &self.overrides
335    }
336
337    /// The content type of `part_name` (7.2.3.5): a matching Override wins,
338    /// then a Default matching the extension; both compared ASCII case-insensitively.
339    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    /// The content type a Default declares for `extension`, if any.
350    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    /// The content type an Override declares for exactly `part_name`, if any.
357    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/// `TargetMode` of a relationship (6.5.3.4).
365#[derive(Clone, Copy, Debug, PartialEq, Eq)]
366pub enum TargetMode {
367    Internal,
368    External,
369}
370
371/// One `Relationship` element, as written.
372#[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    /// Byte offset of the element in the Relationships part.
379    pub offset: usize,
380}
381
382/// A parsed Relationships part (6.5).
383#[derive(Clone, Debug, Default)]
384pub struct Relationships {
385    /// The source part these relationships belong to; `/` for the package.
386    pub source: String,
387    items: Vec<Relationship>,
388    index: FastMap<String, usize>,
389    /// Indexes of relationships whose Id repeats an earlier one.
390    pub duplicate_ids: Vec<usize>,
391    pub defects: Vec<Defect>,
392    pub wrong_namespace: bool,
393}
394
395impl Relationships {
396    /// An empty relationships set for `source`.
397    pub fn empty(source: &str) -> Self {
398        Self {
399            source: source.to_string(),
400            ..Self::default()
401        }
402    }
403
404    /// Parses a Relationships part belonging to `source`.
405    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    /// The first relationship with this Id.
492    pub fn get(&self, id: &str) -> Option<&Relationship> {
493        self.index.get(id).map(|&index| &self.items[index])
494    }
495
496    /// Relationships of exactly this type (6.5.3.4: compared as strings).
497    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    /// The absolute part name an Internal relationship points at.
508    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    /// The absolute part name behind relationship `id`, if internal.
516    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&amp;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}