1use std::collections::HashMap;
4use std::io::{Read, Seek, SeekFrom, Write};
5use std::path::Path;
6
7use zip::ZipWriter;
8use zip::read::ZipArchive;
9use zip::write::SimpleFileOptions;
10
11use crate::content_types::ContentTypes;
12use crate::error::{OpcError, Result};
13use crate::relationship::{Relationships, rel_types};
14
15const EOCD_SIGNATURE: [u8; 4] = [0x50, 0x4b, 0x05, 0x06];
16const ZIP64_EOCD_LOCATOR_SIGNATURE: [u8; 4] = [0x50, 0x4b, 0x06, 0x07];
17const ZIP64_EOCD_SIGNATURE: [u8; 4] = [0x50, 0x4b, 0x06, 0x06];
18const MAX_ZIP_COMMENT_LEN: u64 = u16::MAX as u64;
19const EOCD_LEN: u64 = 22;
20const ZIP64_EOCD_LOCATOR_LEN: u64 = 20;
21const MIN_ZIP64_EOCD_LEN: u64 = 56;
22const ZIP_TAIL_LEN: u64 =
23 MAX_ZIP_COMMENT_LEN + EOCD_LEN + ZIP64_EOCD_LOCATOR_LEN + MIN_ZIP64_EOCD_LEN;
24
25#[derive(Debug, Clone)]
27pub struct PackagePart {
28 pub name: String,
30 pub data: Vec<u8>,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct PackageReadLimits {
37 pub max_entries: usize,
38 pub max_part_uncompressed_bytes: u64,
39 pub max_total_uncompressed_bytes: u64,
40}
41
42impl PackageReadLimits {
43 pub const UNBOUNDED: Self = Self {
44 max_entries: usize::MAX,
45 max_part_uncompressed_bytes: u64::MAX,
46 max_total_uncompressed_bytes: u64::MAX,
47 };
48}
49
50#[derive(Debug, Clone)]
52pub struct OpcPackage {
53 pub content_types: ContentTypes,
55 pub package_rels: Relationships,
57 pub part_rels: HashMap<String, Relationships>,
61 pub parts: HashMap<String, Vec<u8>>,
63 #[cfg(feature = "digital-signatures")]
64 pub(crate) signature_source: Option<crate::signature::SignatureSource>,
65}
66
67impl OpcPackage {
68 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
70 let file = std::fs::File::open(path)?;
71 Self::from_reader(file)
72 }
73
74 pub fn from_reader<R: Read + Seek>(reader: R) -> Result<Self> {
76 Self::from_reader_with_limits(reader, PackageReadLimits::UNBOUNDED)
77 }
78
79 #[cfg(feature = "agile-encryption")]
81 pub fn from_encrypted_reader<R: Read + Seek>(reader: R, password: &str) -> Result<Self> {
82 Self::from_encrypted_reader_with_limits(reader, password, PackageReadLimits::UNBOUNDED)
83 }
84
85 #[cfg(feature = "agile-encryption")]
87 pub fn from_encrypted_reader_with_limits<R: Read + Seek>(
88 reader: R,
89 password: &str,
90 limits: PackageReadLimits,
91 ) -> Result<Self> {
92 let plaintext = crate::encryption::decrypt_package(reader, password, limits)?;
93 Self::from_reader_with_limits(std::io::Cursor::new(plaintext), limits)
94 }
95
96 pub fn from_reader_with_limits<R: Read + Seek>(
98 mut reader: R,
99 limits: PackageReadLimits,
100 ) -> Result<Self> {
101 if limits.max_entries != usize::MAX {
102 let entry_count = pre_index_entry_count(&mut reader)?;
103 if entry_count > limits.max_entries as u64 {
104 return Err(OpcError::PackageLimitExceeded {
105 kind: "entry count",
106 limit: limits.max_entries as u64,
107 });
108 }
109 }
110
111 let mut archive = ZipArchive::new(reader)?;
112 if archive.len() > limits.max_entries {
113 return Err(OpcError::PackageLimitExceeded {
114 kind: "entry count",
115 limit: limits.max_entries as u64,
116 });
117 }
118 let mut raw_parts: HashMap<String, Vec<u8>> = HashMap::new();
119 let mut total_uncompressed_bytes = 0_u64;
120
121 for i in 0..archive.len() {
123 let mut entry = archive.by_index(i)?;
124 if entry.is_dir() {
125 continue;
126 }
127 if entry.size() > limits.max_part_uncompressed_bytes {
128 return Err(OpcError::PackageLimitExceeded {
129 kind: "part size",
130 limit: limits.max_part_uncompressed_bytes,
131 });
132 }
133 if total_uncompressed_bytes
134 .checked_add(entry.size())
135 .is_none_or(|total| total > limits.max_total_uncompressed_bytes)
136 {
137 return Err(OpcError::PackageLimitExceeded {
138 kind: "total uncompressed size",
139 limit: limits.max_total_uncompressed_bytes,
140 });
141 }
142 let name = normalize_part_name(entry.name());
143 let name = name.strip_prefix('/').unwrap_or(&name).to_string();
144 let mut data = Vec::new();
145 let read_limit = limits
146 .max_part_uncompressed_bytes
147 .min(limits.max_total_uncompressed_bytes - total_uncompressed_bytes);
148 entry
149 .by_ref()
150 .take(read_limit.saturating_add(1))
151 .read_to_end(&mut data)?;
152 let data_len = data.len() as u64;
153 if data_len > limits.max_part_uncompressed_bytes {
154 return Err(OpcError::PackageLimitExceeded {
155 kind: "part size",
156 limit: limits.max_part_uncompressed_bytes,
157 });
158 }
159 total_uncompressed_bytes = total_uncompressed_bytes
160 .checked_add(data_len)
161 .filter(|total| *total <= limits.max_total_uncompressed_bytes)
162 .ok_or(OpcError::PackageLimitExceeded {
163 kind: "total uncompressed size",
164 limit: limits.max_total_uncompressed_bytes,
165 })?;
166 raw_parts.insert(name, data);
167 }
168
169 let ct_xml = raw_parts
171 .get("[Content_Types].xml")
172 .ok_or_else(|| OpcError::PartNotFound("[Content_Types].xml".into()))?;
173 let content_types = ContentTypes::from_xml(ct_xml)?;
174
175 let package_rels = if let Some(rels_xml) = raw_parts.get("_rels/.rels") {
177 Relationships::from_xml(rels_xml)?
178 } else {
179 Relationships::new()
180 };
181
182 let mut part_rels = HashMap::new();
184 let rels_entries: Vec<String> = raw_parts
185 .keys()
186 .filter(|k| k.ends_with(".rels") && *k != "_rels/.rels")
187 .cloned()
188 .collect();
189
190 for rels_path in rels_entries {
191 if let Some(xml_data) = raw_parts.get(&rels_path) {
192 let rels = Relationships::from_xml(xml_data)?;
193 let part_name = rels_path_to_part_name(&rels_path);
196 part_rels.insert(part_name, rels);
197 }
198 }
199
200 let mut parts = HashMap::new();
202 for (name, data) in &raw_parts {
203 if name == "[Content_Types].xml" || name == "_rels/.rels" || name.ends_with(".rels") {
204 continue;
205 }
206 let normalized = if name.starts_with('/') {
207 name.clone()
208 } else {
209 format!("/{name}")
210 };
211 parts.insert(normalized, data.clone());
212 }
213
214 Ok(OpcPackage {
215 content_types,
216 package_rels,
217 part_rels,
218 parts,
219 #[cfg(feature = "digital-signatures")]
220 signature_source: Some(crate::signature::SignatureSource {
221 content_types_xml: ct_xml.clone(),
222 content_types: ContentTypes::from_xml(ct_xml)?,
223 }),
224 })
225 }
226
227 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
229 let file = std::fs::File::create(path)?;
230 self.write_to(file)
231 }
232
233 pub fn write_to<W: Write + Seek>(&self, writer: W) -> Result<()> {
235 let mut zip = ZipWriter::new(writer);
236 let options =
237 SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
238
239 #[cfg(feature = "digital-signatures")]
241 let ct_xml = crate::signature::content_types_bytes(self)?;
242 #[cfg(not(feature = "digital-signatures"))]
243 let ct_xml = self.content_types.to_xml()?;
244 zip.start_file("[Content_Types].xml", options)?;
245 zip.write_all(&ct_xml)?;
246
247 let pkg_rels_xml = self.package_rels.to_xml()?;
249 zip.start_file("_rels/.rels", options)?;
250 zip.write_all(&pkg_rels_xml)?;
251
252 let mut rels_names: Vec<&String> = self.part_rels.keys().collect();
256 rels_names.sort_unstable();
257 for part_name in rels_names {
258 let rels = &self.part_rels[part_name];
259 let rels_path = part_name_to_rels_path(part_name);
260 let rels_xml = rels.to_xml()?;
261 zip.start_file(&rels_path, options)?;
262 zip.write_all(&rels_xml)?;
263 }
264
265 let mut part_names: Vec<&String> = self.parts.keys().collect();
267 part_names.sort_unstable();
268 for name in part_names {
269 let zip_name = name.strip_prefix('/').unwrap_or(name);
271 zip.start_file(zip_name, options)?;
272 zip.write_all(&self.parts[name])?;
273 }
274
275 zip.finish()?;
276 Ok(())
277 }
278
279 #[cfg(feature = "agile-encryption")]
284 pub fn write_encrypted_to(&self, output: &mut Vec<u8>, password: &str) -> Result<()> {
285 let mut plaintext = std::io::Cursor::new(Vec::new());
286 self.write_to(&mut plaintext)?;
287 crate::encryption::write_encrypted_package(output, &plaintext.into_inner(), password)
288 }
289
290 pub fn get_part(&self, part_name: &str) -> Option<&[u8]> {
292 self.parts.get(part_name).map(|v| v.as_slice())
293 }
294
295 pub fn set_part(&mut self, part_name: &str, data: Vec<u8>) {
297 self.parts.insert(part_name.to_string(), data);
298 }
299
300 #[cfg(feature = "digital-signatures")]
306 pub fn verify_signatures(&self) -> Result<Vec<crate::SignatureReport>> {
307 crate::signature::verify_signatures(self)
308 }
309
310 #[cfg(feature = "digital-signatures")]
317 pub fn sign(
318 &mut self,
319 private_key_pkcs8_der: &[u8],
320 certificate_der: &[u8],
321 ) -> Result<crate::SignatureReport> {
322 let mut candidate = self.clone();
323 let report = crate::signature::create_signature(
324 &mut candidate,
325 private_key_pkcs8_der,
326 certificate_der,
327 )?;
328 *self = candidate;
329 Ok(report)
330 }
331
332 pub fn get_part_rels(&self, part_name: &str) -> Option<&Relationships> {
334 self.part_rels.get(part_name)
335 }
336
337 pub fn get_or_create_part_rels(&mut self, part_name: &str) -> &mut Relationships {
339 self.part_rels.entry(part_name.to_string()).or_default()
340 }
341
342 pub fn resolve_rel_target(source_part: &str, rel_target: &str) -> String {
349 let joined = if rel_target.starts_with('/') {
350 rel_target.to_string()
351 } else {
352 let dir = match source_part.rfind('/') {
354 Some(pos) => &source_part[..=pos],
355 None => "/",
356 };
357 format!("{dir}{rel_target}")
358 };
359 normalize_part_name(&joined)
360 }
361
362 pub fn main_document_part(&self) -> Option<String> {
364 self.package_rels
365 .get_by_type(rel_types::DOCUMENT)
366 .map(|rel| {
367 if rel.target.starts_with('/') {
368 rel.target.clone()
369 } else {
370 format!("/{}", rel.target)
371 }
372 })
373 }
374
375 pub fn new() -> Self {
377 OpcPackage {
378 content_types: ContentTypes::minimal(),
379 package_rels: Relationships::new(),
380 part_rels: HashMap::new(),
381 parts: HashMap::new(),
382 #[cfg(feature = "digital-signatures")]
383 signature_source: None,
384 }
385 }
386
387 pub fn with_main_part(part_name: &str, content_type: &str) -> Self {
389 let mut package = Self::new();
390 let part_name = part_name.strip_prefix('/').unwrap_or(part_name);
391 package.package_rels.add(rel_types::DOCUMENT, part_name);
392 package
393 .content_types
394 .add_override(&format!("/{part_name}"), content_type);
395 package
396 }
397}
398
399fn pre_index_entry_count<R: Read + Seek>(reader: &mut R) -> Result<u64> {
400 let file_len = reader.seek(SeekFrom::End(0))?;
401 if file_len < EOCD_LEN {
402 return Err(invalid_zip("end of central directory not found"));
403 }
404
405 let tail_len = file_len.min(ZIP_TAIL_LEN);
406 let tail_start = file_len - tail_len;
407 reader.seek(SeekFrom::Start(tail_start))?;
408 let mut tail = vec![0; tail_len as usize];
409 reader.read_exact(&mut tail)?;
410
411 let last_candidate = tail.len() - EOCD_LEN as usize;
412 let entry_count = (0..=last_candidate)
413 .rev()
414 .filter(|&offset| tail[offset..].starts_with(&EOCD_SIGNATURE))
415 .filter_map(|offset| eocd_entry_count(&tail, tail_start, file_len, offset))
416 .max();
417
418 entry_count.ok_or_else(|| invalid_zip("valid end of central directory not found"))
419}
420
421fn eocd_entry_count(tail: &[u8], tail_start: u64, file_len: u64, offset: usize) -> Option<u64> {
422 let eocd = tail.get(offset..offset.checked_add(EOCD_LEN as usize)?)?;
423 let comment_len = u64::from(read_u16(eocd, 20)?);
424 let absolute_offset = tail_start.checked_add(offset as u64)?;
425 if absolute_offset
426 .checked_add(EOCD_LEN)?
427 .checked_add(comment_len)?
428 != file_len
429 {
430 return None;
431 }
432
433 let disk_number = read_u16(eocd, 4)?;
434 let central_directory_disk = read_u16(eocd, 6)?;
435 if disk_number != 0 || central_directory_disk != 0 {
436 return None;
437 }
438
439 let entries_on_disk = read_u16(eocd, 8)?;
440 let total_entries = read_u16(eocd, 10)?;
441 let central_directory_size = read_u32(eocd, 12)?;
442 let central_directory_offset = read_u32(eocd, 16)?;
443 let uses_zip64 = entries_on_disk == u16::MAX
444 || total_entries == u16::MAX
445 || central_directory_size == u32::MAX
446 || central_directory_offset == u32::MAX;
447
448 if uses_zip64 {
449 return zip64_entry_count(tail, tail_start, absolute_offset);
450 }
451 if entries_on_disk != total_entries
452 || !central_directory_bounds_are_plausible(
453 absolute_offset,
454 u64::from(central_directory_offset),
455 u64::from(central_directory_size),
456 )
457 {
458 return None;
459 }
460
461 Some(u64::from(total_entries))
462}
463
464fn zip64_entry_count(tail: &[u8], tail_start: u64, eocd_offset: u64) -> Option<u64> {
465 let locator_offset = eocd_offset.checked_sub(ZIP64_EOCD_LOCATOR_LEN)?;
466 let locator = tail_slice(tail, tail_start, locator_offset, ZIP64_EOCD_LOCATOR_LEN)?;
467 if !locator.starts_with(&ZIP64_EOCD_LOCATOR_SIGNATURE)
468 || read_u32(locator, 4)? != 0
469 || read_u32(locator, 16)? != 1
470 {
471 return None;
472 }
473
474 let zip64_offset = read_u64(locator, 8)?;
475 let zip64 = tail_slice(tail, tail_start, zip64_offset, MIN_ZIP64_EOCD_LEN)?;
476 if !zip64.starts_with(&ZIP64_EOCD_SIGNATURE) {
477 return None;
478 }
479 let record_size = read_u64(zip64, 4)?;
480 if record_size < 44
481 || zip64_offset.checked_add(12)?.checked_add(record_size)? != locator_offset
482 || read_u32(zip64, 16)? != 0
483 || read_u32(zip64, 20)? != 0
484 {
485 return None;
486 }
487
488 let entries_on_disk = read_u64(zip64, 24)?;
489 let total_entries = read_u64(zip64, 32)?;
490 if entries_on_disk != total_entries
491 || !central_directory_bounds_are_plausible(
492 zip64_offset,
493 read_u64(zip64, 48)?,
494 read_u64(zip64, 40)?,
495 )
496 {
497 return None;
498 }
499
500 Some(total_entries)
501}
502
503fn central_directory_bounds_are_plausible(
504 directory_end: u64,
505 relative_directory_offset: u64,
506 directory_size: u64,
507) -> bool {
508 directory_end
509 .checked_sub(directory_size)
510 .is_some_and(|actual_directory_offset| relative_directory_offset <= actual_directory_offset)
511}
512
513fn tail_slice(tail: &[u8], tail_start: u64, absolute_offset: u64, len: u64) -> Option<&[u8]> {
514 let offset = usize::try_from(absolute_offset.checked_sub(tail_start)?).ok()?;
515 let len = usize::try_from(len).ok()?;
516 tail.get(offset..offset.checked_add(len)?)
517}
518
519fn read_u16(bytes: &[u8], offset: usize) -> Option<u16> {
520 Some(u16::from_le_bytes(
521 bytes.get(offset..offset.checked_add(2)?)?.try_into().ok()?,
522 ))
523}
524
525fn read_u32(bytes: &[u8], offset: usize) -> Option<u32> {
526 Some(u32::from_le_bytes(
527 bytes.get(offset..offset.checked_add(4)?)?.try_into().ok()?,
528 ))
529}
530
531fn read_u64(bytes: &[u8], offset: usize) -> Option<u64> {
532 Some(u64::from_le_bytes(
533 bytes.get(offset..offset.checked_add(8)?)?.try_into().ok()?,
534 ))
535}
536
537fn invalid_zip(detail: &'static str) -> OpcError {
538 OpcError::Zip(zip::result::ZipError::InvalidArchive(detail.into()))
539}
540
541impl Default for OpcPackage {
542 fn default() -> Self {
543 Self::new()
544 }
545}
546
547#[cfg(test)]
548fn docx_package() -> OpcPackage {
549 let mut package = OpcPackage::with_main_part(
550 "word/document.xml",
551 "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
552 );
553 package.content_types.add_override(
554 "/word/styles.xml",
555 "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml",
556 );
557 package
558}
559
560fn normalize_part_name(path: &str) -> String {
565 if !path.contains("./") && !path.ends_with("/.") && !path.ends_with("/..") {
566 return path.to_string();
567 }
568
569 let mut segments: Vec<&str> = Vec::new();
570 for segment in path.split('/') {
571 match segment {
572 "" | "." => {}
573 ".." => {
574 segments.pop();
575 }
576 other => segments.push(other),
577 }
578 }
579
580 let mut out = String::with_capacity(path.len());
581 for segment in segments {
582 out.push('/');
583 out.push_str(segment);
584 }
585 if out.is_empty() { "/".to_string() } else { out }
586}
587
588fn rels_path_to_part_name(rels_path: &str) -> String {
591 let without_suffix = rels_path.strip_suffix(".rels").unwrap_or(rels_path);
595 let path = match without_suffix.rfind("_rels/") {
596 Some(pos) => format!(
597 "{}{}",
598 &without_suffix[..pos],
599 &without_suffix[pos + "_rels/".len()..]
600 ),
601 None => without_suffix.to_string(),
602 };
603 if path.starts_with('/') {
604 path
605 } else {
606 format!("/{path}")
607 }
608}
609
610fn part_name_to_rels_path(part_name: &str) -> String {
613 let name = part_name.strip_prefix('/').unwrap_or(part_name);
614 if let Some(pos) = name.rfind('/') {
615 let dir = &name[..pos];
616 let file = &name[pos + 1..];
617 format!("{dir}/_rels/{file}.rels")
618 } else {
619 format!("_rels/{name}.rels")
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626
627 fn package_zip(entries: &[(&str, &[u8])]) -> std::io::Cursor<Vec<u8>> {
628 package_zip_with_comment(entries, &[])
629 }
630
631 fn package_zip_with_comment(
632 entries: &[(&str, &[u8])],
633 comment: &[u8],
634 ) -> std::io::Cursor<Vec<u8>> {
635 let mut buffer = std::io::Cursor::new(Vec::new());
636 {
637 let mut zip = ZipWriter::new(&mut buffer);
638 let options = SimpleFileOptions::default();
639 for (name, data) in entries {
640 zip.start_file(name, options).unwrap();
641 zip.write_all(data).unwrap();
642 }
643 zip.set_raw_comment(comment.to_vec().into_boxed_slice())
644 .unwrap();
645 zip.finish().unwrap();
646 }
647 buffer.set_position(0);
648 buffer
649 }
650
651 fn fake_eocd(total_entries: u16) -> [u8; EOCD_LEN as usize] {
652 let mut eocd = [0; EOCD_LEN as usize];
653 eocd[..4].copy_from_slice(&EOCD_SIGNATURE);
654 eocd[8..10].copy_from_slice(&total_entries.to_le_bytes());
655 eocd[10..12].copy_from_slice(&total_entries.to_le_bytes());
656 eocd
657 }
658
659 const MINIMAL_CONTENT_TYPES: &[u8] =
660 br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
661<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
662 <Default Extension="xml" ContentType="application/xml"/>
663</Types>"#;
664
665 #[test]
666 fn bounded_reader_rejects_too_many_entries() {
667 let archive = package_zip(&[
668 ("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
669 ("word/document.xml", b"<document/>"),
670 ]);
671 let error = OpcPackage::from_reader_with_limits(
672 archive,
673 PackageReadLimits {
674 max_entries: 1,
675 max_part_uncompressed_bytes: 1_024,
676 max_total_uncompressed_bytes: 2_048,
677 },
678 )
679 .unwrap_err();
680
681 assert!(matches!(
682 error,
683 OpcError::PackageLimitExceeded {
684 kind: "entry count",
685 limit: 1
686 }
687 ));
688 }
689
690 #[test]
691 fn later_eocd_signature_in_comment_cannot_hide_real_entry_count() {
692 let mut archive = package_zip_with_comment(
693 &[
694 ("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
695 ("word/document.xml", b"<document/>"),
696 ],
697 &fake_eocd(1),
698 );
699
700 assert_eq!(pre_index_entry_count(&mut archive).unwrap(), 2);
701 }
702
703 #[test]
704 fn truncated_zip64_metadata_returns_an_error_without_panicking() {
705 let mut bytes = vec![0; 4 + ZIP64_EOCD_LOCATOR_LEN as usize + EOCD_LEN as usize];
706 bytes[..4].copy_from_slice(&ZIP64_EOCD_SIGNATURE);
707
708 let locator_offset = 4;
709 bytes[locator_offset..locator_offset + 4].copy_from_slice(&ZIP64_EOCD_LOCATOR_SIGNATURE);
710 bytes[locator_offset + 8..locator_offset + 16].copy_from_slice(&0_u64.to_le_bytes());
711 bytes[locator_offset + 16..locator_offset + 20].copy_from_slice(&1_u32.to_le_bytes());
712
713 let eocd_offset = locator_offset + ZIP64_EOCD_LOCATOR_LEN as usize;
714 bytes[eocd_offset..eocd_offset + 4].copy_from_slice(&EOCD_SIGNATURE);
715 bytes[eocd_offset + 8..eocd_offset + 12].fill(0xff);
716 bytes[eocd_offset + 12..eocd_offset + 20].fill(0xff);
717
718 let error = pre_index_entry_count(&mut std::io::Cursor::new(bytes)).unwrap_err();
719 assert!(matches!(error, OpcError::Zip(_)));
720 }
721
722 #[test]
723 fn bounded_reader_rejects_oversized_parts_and_totals() {
724 let entries = [
725 ("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
726 ("word/document.xml", b"<document/>".as_slice()),
727 ];
728 let part_error = OpcPackage::from_reader_with_limits(
729 package_zip(&entries),
730 PackageReadLimits {
731 max_entries: 8,
732 max_part_uncompressed_bytes: 16,
733 max_total_uncompressed_bytes: 1_024,
734 },
735 )
736 .unwrap_err();
737 assert!(matches!(
738 part_error,
739 OpcError::PackageLimitExceeded {
740 kind: "part size",
741 limit: 16
742 }
743 ));
744
745 let total_error = OpcPackage::from_reader_with_limits(
746 package_zip(&entries),
747 PackageReadLimits {
748 max_entries: 8,
749 max_part_uncompressed_bytes: 1_024,
750 max_total_uncompressed_bytes: MINIMAL_CONTENT_TYPES.len() as u64,
751 },
752 )
753 .unwrap_err();
754 assert!(matches!(
755 total_error,
756 OpcError::PackageLimitExceeded {
757 kind: "total uncompressed size",
758 ..
759 }
760 ));
761 }
762
763 fn independently_built_pptx() -> std::io::Cursor<Vec<u8>> {
764 const CONTENT_TYPES: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
765<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
766 <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
767 <Default Extension="xml" ContentType="application/xml"/>
768 <Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
769 <Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
770 <Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>
771 <Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>
772 <Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
773</Types>"#;
774 const PACKAGE_RELS: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
775<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
776 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>
777</Relationships>"#;
778 const PRESENTATION_RELS: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
779<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
780 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>
781 <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
782</Relationships>"#;
783 const SLIDE_RELS: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
784<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
785 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
786</Relationships>"#;
787 const SLIDE_LAYOUT_RELS: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
788<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
789 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>
790</Relationships>"#;
791 const SLIDE_MASTER_RELS: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
792<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
793 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
794 <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>
795</Relationships>"#;
796 const PRESENTATION: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
797<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
798 <p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>
799 <p:sldIdLst><p:sldId id="256" r:id="rId2"/></p:sldIdLst>
800 <p:sldSz cx="12192000" cy="6858000"/>
801 <p:notesSz cx="6858000" cy="9144000"/>
802</p:presentation>"#;
803 const SLIDE: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
804<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
805 <p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld>
806 <p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>
807</p:sld>"#;
808 const SLIDE_LAYOUT: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
809<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
810 <p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld>
811 <p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>
812</p:sldLayout>"#;
813 const SLIDE_MASTER: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
814<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
815 <p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld>
816 <p:clrMap accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" bg1="lt1" bg2="lt2" folHlink="folHlink" hlink="hlink" tx1="dk1" tx2="dk2"/>
817 <p:sldLayoutIdLst><p:sldLayoutId id="2147483648" r:id="rId1"/></p:sldLayoutIdLst>
818 <p:txStyles><p:titleStyle/><p:bodyStyle/><p:otherStyle/></p:txStyles>
819</p:sldMaster>"#;
820 const THEME: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
821<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Minimal">
822 <a:themeElements>
823 <a:clrScheme name="Minimal">
824 <a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
825 <a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
826 <a:dk2><a:srgbClr val="1F497D"/></a:dk2>
827 <a:lt2><a:srgbClr val="EEECE1"/></a:lt2>
828 <a:accent1><a:srgbClr val="4F81BD"/></a:accent1>
829 <a:accent2><a:srgbClr val="C0504D"/></a:accent2>
830 <a:accent3><a:srgbClr val="9BBB59"/></a:accent3>
831 <a:accent4><a:srgbClr val="8064A2"/></a:accent4>
832 <a:accent5><a:srgbClr val="4BACC6"/></a:accent5>
833 <a:accent6><a:srgbClr val="F79646"/></a:accent6>
834 <a:hlink><a:srgbClr val="0000FF"/></a:hlink>
835 <a:folHlink><a:srgbClr val="800080"/></a:folHlink>
836 </a:clrScheme>
837 <a:fontScheme name="Minimal">
838 <a:majorFont><a:latin typeface="Arial"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>
839 <a:minorFont><a:latin typeface="Arial"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>
840 </a:fontScheme>
841 <a:fmtScheme name="Minimal">
842 <a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:fillStyleLst>
843 <a:lnStyleLst><a:ln w="6350"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="12700"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="19050"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln></a:lnStyleLst>
844 <a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst>
845 <a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:bgFillStyleLst>
846 </a:fmtScheme>
847 </a:themeElements>
848</a:theme>"#;
849
850 package_zip(&[
851 ("[Content_Types].xml", CONTENT_TYPES),
852 ("_rels/.rels", PACKAGE_RELS),
853 ("ppt/presentation.xml", PRESENTATION),
854 ("ppt/_rels/presentation.xml.rels", PRESENTATION_RELS),
855 ("ppt/slides/slide1.xml", SLIDE),
856 ("ppt/slides/_rels/slide1.xml.rels", SLIDE_RELS),
857 ("ppt/slideLayouts/slideLayout1.xml", SLIDE_LAYOUT),
858 (
859 "ppt/slideLayouts/_rels/slideLayout1.xml.rels",
860 SLIDE_LAYOUT_RELS,
861 ),
862 ("ppt/slideMasters/slideMaster1.xml", SLIDE_MASTER),
863 (
864 "ppt/slideMasters/_rels/slideMaster1.xml.rels",
865 SLIDE_MASTER_RELS,
866 ),
867 ("ppt/theme/theme1.xml", THEME),
868 ])
869 }
870
871 fn pptx_package() -> OpcPackage {
872 let mut package =
873 OpcPackage::with_main_part("ppt/presentation.xml", crate::content_types::PRESENTATION);
874 package
875 .content_types
876 .add_override("/ppt/slides/slide1.xml", crate::content_types::SLIDE);
877 package.content_types.add_override(
878 "/ppt/slideLayouts/slideLayout1.xml",
879 crate::content_types::SLIDE_LAYOUT,
880 );
881 package.set_part("/ppt/presentation.xml", b"<p:presentation/>".to_vec());
882 package.set_part("/ppt/slides/slide1.xml", b"<p:sld/>".to_vec());
883 package.set_part(
884 "/ppt/slideLayouts/slideLayout1.xml",
885 b"<p:sldLayout/>".to_vec(),
886 );
887 package
888 .get_or_create_part_rels("/ppt/presentation.xml")
889 .add(rel_types::SLIDE, "slides/slide1.xml");
890 package
891 .get_or_create_part_rels("/ppt/slides/slide1.xml")
892 .add(rel_types::SLIDE_LAYOUT, "../slideLayouts/slideLayout1.xml");
893 package
894 }
895
896 #[test]
897 fn with_main_part_resolves_and_round_trips() {
898 let mut package = OpcPackage::with_main_part(
899 "ppt/presentation.xml",
900 "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml",
901 );
902 package.set_part("/ppt/presentation.xml", b"<p:presentation/>".to_vec());
903
904 assert_eq!(
905 package.main_document_part().as_deref(),
906 Some("/ppt/presentation.xml")
907 );
908 assert_eq!(
909 package
910 .content_types
911 .content_type_for("/ppt/presentation.xml"),
912 Some(
913 "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"
914 )
915 );
916
917 let mut buffer = std::io::Cursor::new(Vec::new());
918 package.write_to(&mut buffer).unwrap();
919 buffer.set_position(0);
920 let round_tripped = OpcPackage::from_reader(buffer).unwrap();
921
922 assert_eq!(
923 round_tripped.main_document_part().as_deref(),
924 Some("/ppt/presentation.xml")
925 );
926 assert_eq!(
927 round_tripped.get_part("/ppt/presentation.xml"),
928 Some(b"<p:presentation/>".as_slice())
929 );
930 }
931
932 #[test]
933 fn pptx_package_resolves_main_slide_and_layout_parts() {
934 let package = pptx_package();
935 let mut buffer = std::io::Cursor::new(Vec::new());
936 package.write_to(&mut buffer).unwrap();
937 buffer.set_position(0);
938 let reopened = OpcPackage::from_reader(buffer).unwrap();
939
940 let presentation_part = reopened.main_document_part().unwrap();
941 assert_eq!(presentation_part, "/ppt/presentation.xml");
942
943 let slide_target = &reopened
944 .get_part_rels(&presentation_part)
945 .unwrap()
946 .get_by_type(rel_types::SLIDE)
947 .unwrap()
948 .target;
949 let slide_part = OpcPackage::resolve_rel_target(&presentation_part, slide_target);
950 assert_eq!(slide_part, "/ppt/slides/slide1.xml");
951
952 let layout_target = &reopened
953 .get_part_rels(&slide_part)
954 .unwrap()
955 .get_by_type(rel_types::SLIDE_LAYOUT)
956 .unwrap()
957 .target;
958 let layout_part = OpcPackage::resolve_rel_target(&slide_part, layout_target);
959 assert_eq!(layout_part, "/ppt/slideLayouts/slideLayout1.xml");
960
961 assert!(reopened.parts.contains_key(&presentation_part));
962 assert!(reopened.parts.contains_key(&slide_part));
963 assert!(reopened.parts.contains_key(&layout_part));
964 }
965
966 #[test]
967 fn independently_built_pptx_opens_and_resolves_relationships() {
968 let package = OpcPackage::from_reader(independently_built_pptx()).unwrap();
969
970 let presentation_part = package.main_document_part().unwrap();
971 assert_eq!(presentation_part, "/ppt/presentation.xml");
972
973 let presentation_master_target = &package
974 .get_part_rels(&presentation_part)
975 .unwrap()
976 .get_by_type(rel_types::SLIDE_MASTER)
977 .unwrap()
978 .target;
979 let presentation_master_part =
980 OpcPackage::resolve_rel_target(&presentation_part, presentation_master_target);
981 assert_eq!(
982 presentation_master_part,
983 "/ppt/slideMasters/slideMaster1.xml"
984 );
985
986 let slide_target = &package
987 .get_part_rels(&presentation_part)
988 .unwrap()
989 .get_by_type(rel_types::SLIDE)
990 .unwrap()
991 .target;
992 let slide_part = OpcPackage::resolve_rel_target(&presentation_part, slide_target);
993 assert_eq!(slide_part, "/ppt/slides/slide1.xml");
994
995 let layout_target = &package
996 .get_part_rels(&slide_part)
997 .unwrap()
998 .get_by_type(rel_types::SLIDE_LAYOUT)
999 .unwrap()
1000 .target;
1001 let layout_part = OpcPackage::resolve_rel_target(&slide_part, layout_target);
1002 assert_eq!(layout_part, "/ppt/slideLayouts/slideLayout1.xml");
1003
1004 let master_target = &package
1005 .get_part_rels(&layout_part)
1006 .unwrap()
1007 .get_by_type(rel_types::SLIDE_MASTER)
1008 .unwrap()
1009 .target;
1010 let master_part = OpcPackage::resolve_rel_target(&layout_part, master_target);
1011 assert_eq!(master_part, "/ppt/slideMasters/slideMaster1.xml");
1012 assert_eq!(master_part, presentation_master_part);
1013
1014 let master_layout_target = &package
1015 .get_part_rels(&master_part)
1016 .unwrap()
1017 .get_by_type(rel_types::SLIDE_LAYOUT)
1018 .unwrap()
1019 .target;
1020 let master_layout_part = OpcPackage::resolve_rel_target(&master_part, master_layout_target);
1021 assert_eq!(master_layout_part, layout_part);
1022
1023 let theme_target = &package
1024 .get_part_rels(&master_part)
1025 .unwrap()
1026 .get_by_type(rel_types::THEME)
1027 .unwrap()
1028 .target;
1029 let theme_part = OpcPackage::resolve_rel_target(&master_part, theme_target);
1030 assert_eq!(theme_part, "/ppt/theme/theme1.xml");
1031
1032 let master_xml = std::str::from_utf8(package.parts.get(&master_part).unwrap()).unwrap();
1033 let layout_id = master_xml
1034 .split_once("<p:sldLayoutId id=\"")
1035 .unwrap()
1036 .1
1037 .split_once('"')
1038 .unwrap()
1039 .0
1040 .parse::<u64>()
1041 .unwrap();
1042 assert!(layout_id >= 2_147_483_648);
1043
1044 assert!(package.parts.contains_key(&presentation_part));
1045 assert!(package.parts.contains_key(&slide_part));
1046 assert!(package.parts.contains_key(&layout_part));
1047 assert!(package.parts.contains_key(&master_part));
1048 assert!(package.parts.contains_key(&theme_part));
1049 }
1050
1051 #[test]
1052 fn presentation_layout_target_resolves_one_directory_up() {
1053 assert_eq!(
1054 OpcPackage::resolve_rel_target(
1055 "/ppt/slides/slide1.xml",
1056 "../slideLayouts/slideLayout1.xml"
1057 ),
1058 "/ppt/slideLayouts/slideLayout1.xml"
1059 );
1060 }
1061
1062 #[test]
1063 fn rels_path_conversion() {
1064 assert_eq!(
1065 rels_path_to_part_name("word/_rels/document.xml.rels"),
1066 "/word/document.xml"
1067 );
1068 assert_eq!(
1069 part_name_to_rels_path("/word/document.xml"),
1070 "word/_rels/document.xml.rels"
1071 );
1072 }
1073
1074 #[test]
1075 fn resolve_relative_target() {
1076 assert_eq!(
1077 OpcPackage::resolve_rel_target("/word/document.xml", "styles.xml"),
1078 "/word/styles.xml"
1079 );
1080 assert_eq!(
1081 OpcPackage::resolve_rel_target("/word/document.xml", "/word/styles.xml"),
1082 "/word/styles.xml"
1083 );
1084 }
1085
1086 #[test]
1087 fn resolve_target_collapses_parent_segments() {
1088 assert_eq!(
1090 OpcPackage::resolve_rel_target("/word/charts/chart1.xml", "../media/image1.png"),
1091 "/word/media/image1.png"
1092 );
1093 assert_eq!(
1094 OpcPackage::resolve_rel_target("/word/document.xml", "./styles.xml"),
1095 "/word/styles.xml"
1096 );
1097 assert_eq!(
1098 OpcPackage::resolve_rel_target("/word/document.xml", "../../../etc/passwd"),
1099 "/etc/passwd"
1100 );
1101 }
1102
1103 #[test]
1104 fn zip_entry_that_escapes_root_is_clamped_to_root() {
1105 let archive = package_zip(&[
1106 ("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
1107 ("../../etc/passwd", b"not a real password file"),
1108 ]);
1109
1110 let package = OpcPackage::from_reader(archive).unwrap();
1111
1112 assert_eq!(
1113 package.get_part("/etc/passwd"),
1114 Some(b"not a real password file".as_slice())
1115 );
1116 assert!(package.parts.keys().all(|name| !name.contains("..")));
1117 }
1118
1119 #[test]
1120 fn absolute_zip_entry_is_normalized_to_package_root() {
1121 let archive = package_zip(&[
1122 ("/[Content_Types].xml", MINIMAL_CONTENT_TYPES),
1123 ("/absolute/path.xml", b"<absolute/>"),
1124 ]);
1125
1126 let package = OpcPackage::from_reader(archive).unwrap();
1127
1128 assert_eq!(
1129 package.get_part("/absolute/path.xml"),
1130 Some(b"<absolute/>".as_slice())
1131 );
1132 assert_eq!(
1133 package
1134 .parts
1135 .keys()
1136 .filter(|name| name.as_str() == "/absolute/path.xml")
1137 .count(),
1138 1
1139 );
1140 }
1141
1142 #[test]
1143 fn rels_path_suffix_is_stripped_once() {
1144 assert_eq!(
1145 rels_path_to_part_name("word/_rels/document.xml.rels"),
1146 "/word/document.xml"
1147 );
1148 assert_eq!(
1150 rels_path_to_part_name("word/_rels/odd.rels.rels"),
1151 "/word/odd.rels"
1152 );
1153 }
1154
1155 #[test]
1156 fn saved_packages_are_byte_identical() {
1157 let mut pkg = docx_package();
1158 for i in 0..40 {
1159 pkg.set_part(&format!("/word/media/image{i}.png"), vec![i as u8]);
1160 }
1161 pkg.get_or_create_part_rels("/word/document.xml")
1162 .add(rel_types::STYLES, "styles.xml");
1163
1164 let write = || {
1165 let mut buf = std::io::Cursor::new(Vec::new());
1166 pkg.write_to(&mut buf).unwrap();
1167 buf.into_inner()
1168 };
1169 assert_eq!(write(), write());
1170 }
1171
1172 #[test]
1173 fn new_docx_package() {
1174 let pkg = docx_package();
1175 assert!(pkg.main_document_part().is_some());
1176 assert_eq!(pkg.main_document_part().unwrap(), "/word/document.xml");
1177 }
1178
1179 #[test]
1180 fn round_trip_package() {
1181 let mut pkg = docx_package();
1182 pkg.set_part("/word/document.xml", b"<document/>".to_vec());
1183
1184 let mut buf = std::io::Cursor::new(Vec::new());
1186 pkg.write_to(&mut buf).unwrap();
1187
1188 buf.set_position(0);
1190 let pkg2 = OpcPackage::from_reader(buf).unwrap();
1191 assert_eq!(
1192 pkg2.get_part("/word/document.xml"),
1193 Some(b"<document/>".as_slice())
1194 );
1195 assert!(pkg2.main_document_part().is_some());
1196 }
1197}