1use 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#[derive(Clone, Debug)]
27pub struct Part {
28 pub name: String,
30 pub entry: usize,
32 pub pieces: Vec<usize>,
34}
35
36#[derive(Clone, Debug, Default)]
38pub struct PackageDefects {
39 pub content_types_missing: bool,
41 pub content_types_case: Option<String>,
43 pub content_types_error: Option<XmlError>,
45 pub content_types_unreadable: Option<String>,
47 pub invalid_names: Vec<(String, PartNameError)>,
49 pub collisions: Vec<(String, String)>,
51 pub derivable: Vec<(String, String)>,
53 pub directories: Vec<String>,
55 pub incomplete_pieces: Vec<String>,
57}
58
59struct Shared {
60 archive: Archive,
61 parts: Vec<Part>,
62 index: FastMap<String, usize>,
63 content_types_entries: Vec<usize>,
67 content_types: std::sync::OnceLock<ParsedContentTypes>,
68 base_defects: PackageDefects,
71 defects: std::sync::OnceLock<PackageDefects>,
72}
73
74type PieceSequence = (String, Vec<(u64, bool, usize)>);
76
77#[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#[derive(Clone)]
111pub struct PackageSeed(Arc<Shared>);
112
113pub 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 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 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 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 pub fn seed(&self) -> PackageSeed {
250 PackageSeed(Arc::clone(&self.shared))
251 }
252
253 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 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 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 pub fn part(&self, name: &str) -> Option<&Part> {
304 self.part_index(name).map(|i| &self.shared.parts[i])
305 }
306
307 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 pub fn content_type_of(&self, name: &str) -> Option<&str> {
315 self.content_types().content_type_of(name)
316 }
317
318 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 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 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 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 pub fn resolve(&self, source: &str, id: &str) -> Result<Option<String>> {
403 Ok(self.rels(source)?.target_of(id))
404 }
405}
406
407fn 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
418fn 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}