Skip to main content

pptxboss_core/
package.rs

1//! The raw Open Packaging Conventions view of a ZIP package (ECMA-376
2//! Part 2, clause 7): every item mapped to a part name, the content types
3//! stream, and relationships parsed on demand.
4//!
5//! Nothing here interprets PresentationML. Defects such as invalid part
6//! names, case-insensitive name collisions or a missing content types
7//! stream are recorded, not repaired, so the verifier can report them
8//! while [`crate::Document`] reads around them.
9
10use std::cell::RefCell;
11use std::path::Path;
12use std::rc::Rc;
13use std::sync::Arc;
14
15use crate::encoding::utf16_xml_to_utf8;
16use crate::error::{Error, Result};
17use crate::hash::FastMap;
18use crate::opc::{
19    equivalence_key, rels_part_name, validate_part_name, ContentTypes, PartNameError,
20    Relationships, CONTENT_TYPES_ITEM,
21};
22use crate::xml::XmlError;
23use crate::zip::{Archive, Entry, Source};
24
25/// A ZIP item that maps to a part.
26#[derive(Clone, Debug)]
27pub struct Part {
28    /// The part name: `/` followed by the item name (7.3.5).
29    pub name: String,
30    /// Index of the backing entry in the archive; the first piece of an interleaved part.
31    pub entry: usize,
32    /// Every piece of an interleaved part (7.2.4) in piece order; empty otherwise.
33    pub pieces: Vec<usize>,
34}
35
36/// What the package layer found wrong with the container's item names.
37#[derive(Clone, Debug, Default)]
38pub struct PackageDefects {
39    /// The content types stream item is absent.
40    pub content_types_missing: bool,
41    /// The content types stream item exists under a different case than `[Content_Types].xml`.
42    pub content_types_case: Option<String>,
43    /// The content types stream could not be parsed.
44    pub content_types_error: Option<XmlError>,
45    /// The content types stream could not be decompressed or read.
46    pub content_types_unreadable: Option<String>,
47    /// Parts whose names violate the grammar of 6.2.2.2, with the reason.
48    pub invalid_names: Vec<(String, PartNameError)>,
49    /// Pairs of part names that are equivalent under ASCII case folding (6.2.2.3); the second loses.
50    pub collisions: Vec<(String, String)>,
51    /// Pairs `(derived, base)` where one part name is derivable from another (6.2.2.3).
52    pub derivable: Vec<(String, String)>,
53    /// Entries that are directories; a package has no directories.
54    pub directories: Vec<String>,
55    /// Logical items whose `[n].piece` sequence is incomplete (7.2.5.2); not mapped to parts.
56    pub incomplete_pieces: Vec<String>,
57}
58
59struct Shared {
60    archive: Archive,
61    parts: Vec<Part>,
62    index: FastMap<String, usize>,
63    /// The archive entries of `[Content_Types].xml` (several when
64    /// interleaved), parsed on first use: reading a deck through its
65    /// relationships never needs it.
66    content_types_entries: Vec<usize>,
67    content_types: std::sync::OnceLock<ParsedContentTypes>,
68    /// Defects found while indexing; the name-grammar and derivability
69    /// checks are added on first request, since only the verifier reads them.
70    base_defects: PackageDefects,
71    defects: std::sync::OnceLock<PackageDefects>,
72}
73
74/// A logical item's display name and its pieces as `(number, is last, entry)`.
75type PieceSequence = (String, Vec<(u64, bool, usize)>);
76
77/// The content types stream with what went wrong while reading it.
78#[derive(Default)]
79struct ParsedContentTypes {
80    types: ContentTypes,
81    error: Option<XmlError>,
82    unreadable: Option<String>,
83}
84
85impl ParsedContentTypes {
86    fn read(archive: &Archive, entries: &[usize]) -> Self {
87        if entries.is_empty() {
88            return Self::default();
89        }
90        match read_entries(archive, entries) {
91            Err(err) => Self {
92                unreadable: Some(err.to_string()),
93                ..Self::default()
94            },
95            Ok(bytes) => match ContentTypes::parse(&bytes) {
96                Ok(types) => Self {
97                    types,
98                    ..Self::default()
99                },
100                Err(err) => Self {
101                    error: Some(err),
102                    ..Self::default()
103                },
104            },
105        }
106    }
107}
108
109/// A thread-safe handle from which a [`Package`] with fresh caches is made.
110#[derive(Clone)]
111pub struct PackageSeed(Arc<Shared>);
112
113/// The raw package: parts, content types, relationships.
114pub struct Package {
115    shared: Arc<Shared>,
116    rels: RefCell<FastMap<String, Rc<Relationships>>>,
117    data: RefCell<FastMap<usize, Rc<Vec<u8>>>>,
118}
119
120impl Package {
121    /// Opens a package file with positioned reads.
122    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
123        let path = path.as_ref();
124        let archive = match Archive::open_path(path) {
125            Err(Error::NotZip) => {
126                return Err(classify_not_zip(&std::fs::read(path).unwrap_or_default()))
127            }
128            other => other?,
129        };
130        Self::from_archive(archive)
131    }
132
133    /// Opens a package held in memory.
134    pub fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
135        if bytes.starts_with(&[0xd0, 0xcf, 0x11, 0xe0]) {
136            return Err(Error::CompoundFile);
137        }
138        Self::from_archive(Archive::from_bytes(bytes)?)
139    }
140
141    /// Opens a package over any positioned source.
142    pub fn from_source(source: Arc<dyn Source>) -> Result<Self> {
143        Self::from_archive(Archive::open(source)?)
144    }
145
146    pub fn from_archive(archive: Archive) -> Result<Self> {
147        let mut parts = Vec::with_capacity(archive.entries().len());
148        let mut index: FastMap<String, usize> =
149            FastMap::with_capacity_and_hasher(archive.entries().len(), Default::default());
150        let mut defects = PackageDefects::default();
151        let mut content_types_entries = Vec::new();
152        let mut pieces: FastMap<String, PieceSequence> = FastMap::default();
153        for (i, entry) in archive.entries().iter().enumerate() {
154            if entry.is_directory() {
155                defects.directories.push(entry.name.clone());
156                continue;
157            }
158            if let Some((prefix_len, number, last)) = piece_suffix(&entry.name) {
159                let logical = format!("/{}", &entry.name[..prefix_len]);
160                pieces
161                    .entry(equivalence_key(&logical))
162                    .or_insert_with(|| (logical, Vec::new()))
163                    .1
164                    .push((number, last, i));
165                continue;
166            }
167            if entry.name.eq_ignore_ascii_case(CONTENT_TYPES_ITEM) {
168                if entry.name != CONTENT_TYPES_ITEM {
169                    defects.content_types_case = Some(entry.name.clone());
170                }
171                if content_types_entries.is_empty() {
172                    content_types_entries.push(i);
173                }
174                continue;
175            }
176            let name = format!("/{}", entry.name);
177            let key = equivalence_key(&name);
178            if let Some(&existing) = index.get(&key) {
179                let existing: &Part = &parts[existing];
180                if existing.name != name {
181                    defects.collisions.push((existing.name.clone(), name));
182                }
183                continue;
184            }
185            index.insert(key, parts.len());
186            parts.push(Part {
187                name,
188                entry: i,
189                pieces: Vec::new(),
190            });
191        }
192        let mut sequences: Vec<(String, PieceSequence)> = pieces.into_iter().collect();
193        sequences.sort_by(|a, b| a.0.cmp(&b.0));
194        for (key, (logical, mut items)) in sequences {
195            items.sort_by_key(|(number, _, _)| *number);
196            let complete = items
197                .iter()
198                .enumerate()
199                .all(|(position, (number, last, _))| {
200                    *number == position as u64 && *last == (position + 1 == items.len())
201                });
202            if !complete {
203                defects.incomplete_pieces.push(logical);
204                continue;
205            }
206            let entries: Vec<usize> = items.iter().map(|(_, _, entry)| *entry).collect();
207            if logical[1..].eq_ignore_ascii_case(CONTENT_TYPES_ITEM) {
208                if content_types_entries.is_empty() {
209                    content_types_entries = entries;
210                }
211                continue;
212            }
213            if let Some(&existing) = index.get(&key) {
214                defects
215                    .collisions
216                    .push((parts[existing].name.clone(), logical));
217                continue;
218            }
219            index.insert(key, parts.len());
220            parts.push(Part {
221                name: logical,
222                entry: entries[0],
223                pieces: entries,
224            });
225        }
226
227        defects.content_types_missing = content_types_entries.is_empty();
228        let shared = Arc::new(Shared {
229            archive,
230            parts,
231            index,
232            content_types_entries,
233            content_types: std::sync::OnceLock::new(),
234            base_defects: defects,
235            defects: std::sync::OnceLock::new(),
236        });
237        Ok(Self::with_shared(shared))
238    }
239
240    fn with_shared(shared: Arc<Shared>) -> Self {
241        Self {
242            shared,
243            rels: RefCell::new(FastMap::default()),
244            data: RefCell::new(FastMap::default()),
245        }
246    }
247
248    /// A handle that can cross threads; see [`Package::from_seed`].
249    pub fn seed(&self) -> PackageSeed {
250        PackageSeed(Arc::clone(&self.shared))
251    }
252
253    /// A package sharing the archive and parsed directory of `seed`, with its own caches.
254    pub fn from_seed(seed: PackageSeed) -> Self {
255        Self::with_shared(seed.0)
256    }
257
258    pub fn archive(&self) -> &Archive {
259        &self.shared.archive
260    }
261
262    /// Every part in archive order.
263    pub fn parts(&self) -> &[Part] {
264        &self.shared.parts
265    }
266
267    fn parsed_content_types(&self) -> &ParsedContentTypes {
268        self.shared.content_types.get_or_init(|| {
269            ParsedContentTypes::read(&self.shared.archive, &self.shared.content_types_entries)
270        })
271    }
272
273    pub fn content_types(&self) -> &ContentTypes {
274        &self.parsed_content_types().types
275    }
276
277    pub fn defects(&self) -> &PackageDefects {
278        self.shared.defects.get_or_init(|| {
279            let mut defects = self.shared.base_defects.clone();
280            let parsed = self.parsed_content_types();
281            defects.content_types_error = parsed.error.clone();
282            defects.content_types_unreadable = parsed.unreadable.clone();
283            for part in &self.shared.parts {
284                if let Err(reason) = validate_part_name(&part.name) {
285                    defects.invalid_names.push((part.name.clone(), reason));
286                }
287            }
288            find_derivable(&self.shared.parts, &mut defects);
289            defects
290        })
291    }
292
293    /// The index of the part named `name`, compared ASCII case-insensitively.
294    pub fn part_index(&self, name: &str) -> Option<usize> {
295        self.shared.index.get(&equivalence_key(name)).copied()
296    }
297
298    pub fn has_part(&self, name: &str) -> bool {
299        self.part_index(name).is_some()
300    }
301
302    /// The part as written, found case-insensitively.
303    pub fn part(&self, name: &str) -> Option<&Part> {
304        self.part_index(name).map(|i| &self.shared.parts[i])
305    }
306
307    /// The archive entry backing `name`.
308    pub fn entry(&self, name: &str) -> Option<&Entry> {
309        self.part(name)
310            .and_then(|part| self.shared.archive.get(part.entry))
311    }
312
313    /// The declared content type of `name` (7.2.3.5).
314    pub fn content_type_of(&self, name: &str) -> Option<&str> {
315        self.content_types().content_type_of(name)
316    }
317
318    /// The decompressed bytes of a part, cached for the life of this package.
319    pub fn read_part(&self, name: &str) -> Result<Rc<Vec<u8>>> {
320        let index = self
321            .part_index(name)
322            .ok_or_else(|| Error::MissingPart(name.to_string()))?;
323        if let Some(data) = self.data.borrow().get(&index) {
324            return Ok(Rc::clone(data));
325        }
326        let part = &self.shared.parts[index];
327        let mut bytes = Vec::new();
328        self.read_part_entries(part, &mut bytes)?;
329        let data = Rc::new(bytes);
330        self.data.borrow_mut().insert(index, Rc::clone(&data));
331        Ok(data)
332    }
333
334    /// The decompressed bytes of a part into `out`, bypassing the cache.
335    /// Interleaved pieces are concatenated; a UTF-16 XML part comes back as UTF-8.
336    pub fn read_part_into(&self, name: &str, out: &mut Vec<u8>) -> Result<()> {
337        let index = self
338            .part_index(name)
339            .ok_or_else(|| Error::MissingPart(name.to_string()))?;
340        self.read_part_entries(&self.shared.parts[index], out)
341    }
342
343    /// The decompressed bytes of a part exactly as stored (pieces
344    /// concatenated), without the UTF-16 transcoding of [`Package::read_part`].
345    pub fn read_part_bytes(&self, name: &str, out: &mut Vec<u8>) -> Result<()> {
346        let index = self
347            .part_index(name)
348            .ok_or_else(|| Error::MissingPart(name.to_string()))?;
349        self.read_stored(&self.shared.parts[index], out)
350    }
351
352    fn read_stored(&self, part: &Part, out: &mut Vec<u8>) -> Result<()> {
353        match part.pieces.is_empty() {
354            true => {
355                let entry = &self.shared.archive.entries()[part.entry];
356                self.shared.archive.read(entry, out)
357            }
358            false => {
359                *out = read_entries(&self.shared.archive, &part.pieces)?;
360                Ok(())
361            }
362        }
363    }
364
365    fn read_part_entries(&self, part: &Part, out: &mut Vec<u8>) -> Result<()> {
366        self.read_stored(part, out)?;
367        if let Some(utf8) = utf16_xml_to_utf8(out) {
368            *out = utf8;
369        }
370        Ok(())
371    }
372
373    /// The relationships of `source` (`/` for the package). A missing
374    /// Relationships part yields an empty set; a malformed one is an error.
375    pub fn rels(&self, source: &str) -> Result<Rc<Relationships>> {
376        let key = equivalence_key(source);
377        if let Some(rels) = self.rels.borrow().get(&key) {
378            return Ok(Rc::clone(rels));
379        }
380        let rels_name = rels_part_name(source);
381        let rels = match self.part_index(&rels_name) {
382            None => Relationships::empty(source),
383            Some(_) => {
384                let data = self.read_part(&rels_name)?;
385                Relationships::parse(source, &data).map_err(|err| Error::Xml {
386                    part: rels_name.clone(),
387                    offset: err.offset,
388                    msg: err.msg.to_string(),
389                })?
390            }
391        };
392        let rels = Rc::new(rels);
393        self.rels.borrow_mut().insert(key, Rc::clone(&rels));
394        Ok(rels)
395    }
396
397    pub fn package_rels(&self) -> Result<Rc<Relationships>> {
398        self.rels("/")
399    }
400
401    /// The part name relationship `id` of `source` points at, when internal.
402    pub fn resolve(&self, source: &str, id: &str) -> Result<Option<String>> {
403        Ok(self.rels(source)?.target_of(id))
404    }
405}
406
407/// The concatenated decompressed bytes of `entries`, in order.
408fn read_entries(archive: &Archive, entries: &[usize]) -> Result<Vec<u8>> {
409    let mut out = Vec::new();
410    let mut piece = Vec::new();
411    for &entry in entries {
412        archive.read(&archive.entries()[entry], &mut piece)?;
413        out.extend_from_slice(&piece);
414    }
415    Ok(out)
416}
417
418/// `(prefix length, piece number, is last)` when `name` ends with a piece
419/// suffix `/[n].piece` or `/[n].last.piece` (7.2.5.2), compared ASCII
420/// case-insensitively; piece numbers carry no leading zeros.
421fn piece_suffix(name: &str) -> Option<(usize, u64, bool)> {
422    let lower = name.to_ascii_lowercase();
423    let body = lower.strip_suffix(".piece")?;
424    let (body, last) = match body.strip_suffix(".last") {
425        Some(body) => (body, true),
426        None => (body, false),
427    };
428    let body = body.strip_suffix(']')?;
429    let open = body.rfind("/[")?;
430    let digits = &body[open + 2..];
431    let valid = !digits.is_empty()
432        && digits.bytes().all(|b| b.is_ascii_digit())
433        && (digits == "0" || !digits.starts_with('0'));
434    if !valid || open == 0 {
435        return None;
436    }
437    Some((open, digits.parse().ok()?, last))
438}
439
440fn find_derivable(parts: &[Part], defects: &mut PackageDefects) {
441    let mut keys: Vec<(String, usize)> = parts
442        .iter()
443        .enumerate()
444        .map(|(i, part)| (equivalence_key(&part.name), i))
445        .collect();
446    keys.sort();
447    for window in keys.windows(2) {
448        let (base, base_index) = &window[0];
449        let (next, next_index) = &window[1];
450        if next.len() > base.len() && next.starts_with(base) && next.as_bytes()[base.len()] == b'/'
451        {
452            defects.derivable.push((
453                parts[*next_index].name.clone(),
454                parts[*base_index].name.clone(),
455            ));
456        }
457    }
458}
459
460fn classify_not_zip(head: &[u8]) -> Error {
461    match head.starts_with(&[0xd0, 0xcf, 0x11, 0xe0]) {
462        true => Error::CompoundFile,
463        false => Error::NotZip,
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470    use pptxboss_testkit::ZipBuilder;
471
472    const TYPES: &str = r#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/></Types>"#;
473    const ROOT_RELS: &str = r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/></Relationships>"#;
474    const PRES_RELS: &str = r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>"#;
475
476    fn package() -> Package {
477        let bytes = ZipBuilder::new()
478            .deflated("[Content_Types].xml", TYPES.as_bytes())
479            .deflated("_rels/.rels", ROOT_RELS.as_bytes())
480            .deflated("ppt/presentation.xml", b"<p/>")
481            .deflated("ppt/_rels/presentation.xml.rels", PRES_RELS.as_bytes())
482            .deflated("ppt/slides/slide1.xml", b"<s/>")
483            .stored("ppt/media/image1.png", b"PNG")
484            .build();
485        Package::from_bytes(bytes).unwrap()
486    }
487
488    #[test]
489    fn parts_are_named_with_a_leading_slash_and_found_case_insensitively() {
490        let package = package();
491        let names: Vec<&str> = package
492            .parts()
493            .iter()
494            .map(|part| part.name.as_str())
495            .collect();
496        assert_eq!(
497            names,
498            [
499                "/_rels/.rels",
500                "/ppt/presentation.xml",
501                "/ppt/_rels/presentation.xml.rels",
502                "/ppt/slides/slide1.xml",
503                "/ppt/media/image1.png"
504            ]
505        );
506        assert!(package.has_part("/PPT/Slides/SLIDE1.xml"));
507        assert!(!package.has_part("/[Content_Types].xml"));
508        assert_eq!(package.content_type_of("/ppt/presentation.xml"), Some("application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"));
509        assert_eq!(package.content_type_of("/ppt/media/image1.png"), None);
510        assert!(package.defects().invalid_names.is_empty());
511        assert!(!package.defects().content_types_missing);
512    }
513
514    #[test]
515    fn parts_are_read_and_cached_and_relationships_resolve() {
516        let package = package();
517        let first = package.read_part("/ppt/slides/slide1.xml").unwrap();
518        let second = package.read_part("/ppt/slides/slide1.xml").unwrap();
519        assert!(Rc::ptr_eq(&first, &second));
520        assert_eq!(first.as_slice(), b"<s/>");
521        assert!(matches!(
522            package.read_part("/nope.xml"),
523            Err(Error::MissingPart(_))
524        ));
525        let root = package.package_rels().unwrap();
526        assert_eq!(
527            root.target_of("rId1").as_deref(),
528            Some("/ppt/presentation.xml")
529        );
530        assert_eq!(
531            package
532                .resolve("/ppt/presentation.xml", "rId2")
533                .unwrap()
534                .as_deref(),
535            Some("/ppt/slides/slide1.xml")
536        );
537        let none = package.rels("/ppt/slides/slide1.xml").unwrap();
538        assert!(none.is_empty());
539        assert_eq!(none.source, "/ppt/slides/slide1.xml");
540        let mut out = Vec::new();
541        package
542            .read_part_into("/ppt/media/image1.png", &mut out)
543            .unwrap();
544        assert_eq!(out, b"PNG");
545    }
546
547    #[test]
548    fn a_seed_makes_an_equivalent_package_with_fresh_caches() {
549        let package = package();
550        let seed = package.seed();
551        let other = std::thread::spawn(move || {
552            let package = Package::from_seed(seed);
553            package.read_part("/ppt/presentation.xml").unwrap().to_vec()
554        })
555        .join()
556        .unwrap();
557        assert_eq!(other, b"<p/>");
558    }
559
560    #[test]
561    fn container_defects_are_recorded_not_repaired() {
562        let bytes = ZipBuilder::new()
563            .deflated("[content_types].XML", TYPES.as_bytes())
564            .stored("ppt/", b"")
565            .stored("ppt/slides/slide1.xml", b"<a/>")
566            .stored("PPT/SLIDES/slide1.xml", b"<b/>")
567            .stored("ppt/slides", b"prefix")
568            .stored("bad name.xml", b"")
569            .build();
570        let package = Package::from_bytes(bytes).unwrap();
571        let defects = package.defects();
572        assert_eq!(
573            defects.content_types_case.as_deref(),
574            Some("[content_types].XML")
575        );
576        assert_eq!(defects.directories, vec!["ppt/"]);
577        assert_eq!(
578            defects.collisions,
579            vec![(
580                "/ppt/slides/slide1.xml".to_string(),
581                "/PPT/SLIDES/slide1.xml".to_string()
582            )]
583        );
584        assert_eq!(
585            defects.derivable,
586            vec![(
587                "/ppt/slides/slide1.xml".to_string(),
588                "/ppt/slides".to_string()
589            )]
590        );
591        assert_eq!(defects.invalid_names.len(), 1);
592        assert_eq!(defects.invalid_names[0].0, "/bad name.xml");
593        assert_eq!(
594            package
595                .read_part("/ppt/slides/slide1.xml")
596                .unwrap()
597                .as_slice(),
598            b"<a/>"
599        );
600        assert_eq!(
601            package.content_type_of("/ppt/slides/slide1.xml"),
602            Some("application/xml")
603        );
604    }
605
606    #[test]
607    fn missing_or_broken_content_types_do_not_prevent_opening() {
608        let missing =
609            Package::from_bytes(ZipBuilder::new().stored("a.xml", b"<a/>").build()).unwrap();
610        assert!(missing.defects().content_types_missing);
611        assert_eq!(missing.content_type_of("/a.xml"), None);
612        let broken = Package::from_bytes(
613            ZipBuilder::new()
614                .stored("[Content_Types].xml", b"<Types")
615                .stored("a.xml", b"<a/>")
616                .build(),
617        )
618        .unwrap();
619        assert!(broken.defects().content_types_error.is_some());
620    }
621
622    #[test]
623    fn compound_files_and_non_archives_are_classified() {
624        let mut cfb = vec![0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
625        cfb.resize(512, 0);
626        assert!(matches!(Package::from_bytes(cfb), Err(Error::CompoundFile)));
627        assert!(matches!(
628            Package::from_bytes(b"hello".to_vec()),
629            Err(Error::NotZip)
630        ));
631        let path = std::env::temp_dir().join(format!("pptxboss-cfb-{}.pptx", std::process::id()));
632        let mut cfb = vec![0xd0, 0xcf, 0x11, 0xe0];
633        cfb.resize(64, 0);
634        std::fs::write(&path, &cfb).unwrap();
635        assert!(matches!(Package::open(&path), Err(Error::CompoundFile)));
636        std::fs::remove_file(&path).unwrap();
637    }
638
639    #[test]
640    fn malformed_rels_are_an_error_but_only_when_asked_for() {
641        let bytes = ZipBuilder::new()
642            .stored("[Content_Types].xml", TYPES.as_bytes())
643            .stored("_rels/.rels", b"<Relationships")
644            .stored("ppt/presentation.xml", b"<p/>")
645            .build();
646        let package = Package::from_bytes(bytes).unwrap();
647        assert_eq!(
648            package
649                .read_part("/ppt/presentation.xml")
650                .unwrap()
651                .as_slice(),
652            b"<p/>"
653        );
654        assert!(matches!(package.package_rels(), Err(Error::Xml { .. })));
655    }
656}