1use std::collections::{HashMap, HashSet};
4use std::io::{Read, Seek, SeekFrom, Write};
5use std::path::{Path, PathBuf};
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)]
35pub(crate) struct ContentTypesSource {
36 xml: Vec<u8>,
37 content_types: ContentTypes,
38}
39
40struct SerializedPackage<'a> {
41 content_types: Vec<u8>,
42 package_relationships: Vec<u8>,
43 part_relationships: Vec<(String, Vec<u8>)>,
44 parts: Vec<(&'a str, &'a [u8])>,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct PackageReadLimits {
50 pub max_entries: usize,
51 pub max_part_uncompressed_bytes: u64,
52 pub max_total_uncompressed_bytes: u64,
53}
54
55impl PackageReadLimits {
56 pub const UNBOUNDED: Self = Self {
57 max_entries: usize::MAX,
58 max_part_uncompressed_bytes: u64::MAX,
59 max_total_uncompressed_bytes: u64::MAX,
60 };
61}
62
63#[derive(Debug, Clone)]
65pub struct OpcPackage {
66 pub content_types: ContentTypes,
68 pub package_rels: Relationships,
70 pub part_rels: HashMap<String, Relationships>,
74 pub parts: HashMap<String, Vec<u8>>,
76 content_types_source: Option<ContentTypesSource>,
77 malformed_source_entries: HashMap<String, Vec<u8>>,
81}
82
83impl OpcPackage {
84 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
86 let file = std::fs::File::open(path)?;
87 Self::from_reader(file)
88 }
89
90 pub fn from_reader<R: Read + Seek>(reader: R) -> Result<Self> {
92 Self::from_reader_with_limits(reader, PackageReadLimits::UNBOUNDED)
93 }
94
95 #[cfg(feature = "agile-encryption")]
97 pub fn from_encrypted_reader<R: Read + Seek>(reader: R, password: &str) -> Result<Self> {
98 Self::from_encrypted_reader_with_limits(reader, password, PackageReadLimits::UNBOUNDED)
99 }
100
101 #[cfg(feature = "agile-encryption")]
103 pub fn from_encrypted_reader_with_limits<R: Read + Seek>(
104 reader: R,
105 password: &str,
106 limits: PackageReadLimits,
107 ) -> Result<Self> {
108 let plaintext = crate::encryption::decrypt_package(reader, password, limits)?;
109 Self::from_reader_with_limits(std::io::Cursor::new(plaintext), limits)
110 }
111
112 pub fn from_reader_with_limits<R: Read + Seek>(
114 mut reader: R,
115 limits: PackageReadLimits,
116 ) -> Result<Self> {
117 if limits.max_entries != usize::MAX {
118 let entry_count = pre_index_entry_count(&mut reader)?;
119 if entry_count > limits.max_entries as u64 {
120 return Err(OpcError::PackageLimitExceeded {
121 kind: "entry count",
122 limit: limits.max_entries as u64,
123 });
124 }
125 }
126
127 let mut archive = ZipArchive::new(reader)?;
128 if archive.len() > limits.max_entries {
129 return Err(OpcError::PackageLimitExceeded {
130 kind: "entry count",
131 limit: limits.max_entries as u64,
132 });
133 }
134 let mut raw_parts: HashMap<String, Vec<u8>> = HashMap::new();
135 let mut raw_part_identities = HashSet::new();
136 let mut total_uncompressed_bytes = 0_u64;
137
138 for i in 0..archive.len() {
140 let mut entry = archive.by_index(i)?;
141 if entry.is_dir() {
142 continue;
143 }
144 if entry.size() > limits.max_part_uncompressed_bytes {
145 return Err(OpcError::PackageLimitExceeded {
146 kind: "part size",
147 limit: limits.max_part_uncompressed_bytes,
148 });
149 }
150 if total_uncompressed_bytes
151 .checked_add(entry.size())
152 .is_none_or(|total| total > limits.max_total_uncompressed_bytes)
153 {
154 return Err(OpcError::PackageLimitExceeded {
155 kind: "total uncompressed size",
156 limit: limits.max_total_uncompressed_bytes,
157 });
158 }
159 let name = normalize_part_name(entry.name()?.as_ref());
160 let name = name.strip_prefix('/').unwrap_or(&name).to_string();
161 if !raw_part_identities.insert(name.to_ascii_lowercase()) {
162 return Err(OpcError::DuplicatePartName(name));
163 }
164 let mut data = Vec::new();
165 let read_limit = limits
166 .max_part_uncompressed_bytes
167 .min(limits.max_total_uncompressed_bytes - total_uncompressed_bytes);
168 entry
169 .by_ref()
170 .take(read_limit.saturating_add(1))
171 .read_to_end(&mut data)?;
172 let data_len = data.len() as u64;
173 if data_len > limits.max_part_uncompressed_bytes {
174 return Err(OpcError::PackageLimitExceeded {
175 kind: "part size",
176 limit: limits.max_part_uncompressed_bytes,
177 });
178 }
179 total_uncompressed_bytes = total_uncompressed_bytes
180 .checked_add(data_len)
181 .filter(|total| *total <= limits.max_total_uncompressed_bytes)
182 .ok_or(OpcError::PackageLimitExceeded {
183 kind: "total uncompressed size",
184 limit: limits.max_total_uncompressed_bytes,
185 })?;
186 raw_parts.insert(name, data);
187 }
188
189 let ct_xml = deterministic_part_key(&raw_parts, "/[Content_Types].xml")
191 .and_then(|name| raw_parts.get(name))
192 .ok_or_else(|| OpcError::PartNotFound("[Content_Types].xml".into()))?;
193 let content_types = ContentTypes::from_xml(ct_xml)?;
194
195 let package_rels = if let Some(rels_xml) =
197 deterministic_part_key(&raw_parts, "/_rels/.rels").and_then(|name| raw_parts.get(name))
198 {
199 Relationships::from_xml(rels_xml)?
200 } else {
201 Relationships::new()
202 };
203
204 let mut part_rels = HashMap::new();
206 let rels_entries: Vec<String> = raw_parts
207 .keys()
208 .filter(|name| {
209 name.to_ascii_lowercase().ends_with(".rels")
210 && part_identity(name) != part_identity("/_rels/.rels")
211 })
212 .cloned()
213 .collect();
214
215 for rels_path in rels_entries {
216 if let Some(xml_data) = raw_parts.get(&rels_path) {
217 let rels = Relationships::from_xml(xml_data)?;
218 let part_name = rels_path_to_part_name(&rels_path);
221 part_rels.insert(part_name, rels);
222 }
223 }
224
225 let mut parts = HashMap::new();
227 for (name, data) in &raw_parts {
228 if part_identity(name) == part_identity("/[Content_Types].xml")
229 || part_identity(name) == part_identity("/_rels/.rels")
230 || name.to_ascii_lowercase().ends_with(".rels")
231 {
232 continue;
233 }
234 let normalized = if name.starts_with('/') {
235 name.clone()
236 } else {
237 format!("/{name}")
238 };
239 parts.insert(normalized, data.clone());
240 }
241
242 let malformed_source_entries = raw_parts
243 .iter()
244 .filter(|(name, data)| {
245 is_xml_entry(&content_types, name) && first_invalid_xml_character(data).is_some()
246 })
247 .map(|(name, data)| (name.clone(), data.clone()))
248 .collect();
249
250 Ok(OpcPackage {
251 content_types,
252 package_rels,
253 part_rels,
254 parts,
255 malformed_source_entries,
256 content_types_source: Some(ContentTypesSource {
257 xml: ct_xml.clone(),
258 content_types: ContentTypes::from_xml(ct_xml)?,
259 }),
260 })
261 }
262
263 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
269 let mut output = std::io::Cursor::new(Vec::new());
270 self.write_to(&mut output)?;
271 write_atomic_file(
272 path.as_ref(),
273 output.get_ref(),
274 "oxml-opc",
275 "invalid package file name",
276 "could not allocate package save staging file",
277 )?;
278 Ok(())
279 }
280
281 pub fn write_to<W: Write + Seek>(&self, writer: W) -> Result<()> {
283 Self::write_serialized(writer, self.serialize()?)
284 }
285
286 fn write_serialized<W: Write + Seek>(
287 writer: W,
288 serialized: SerializedPackage<'_>,
289 ) -> Result<()> {
290 let mut zip = ZipWriter::new(writer);
291 let options =
292 SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
293
294 zip.start_file("[Content_Types].xml", options)?;
296 zip.write_all(&serialized.content_types)?;
297
298 zip.start_file("_rels/.rels", options)?;
300 zip.write_all(&serialized.package_relationships)?;
301
302 for (rels_path, rels_xml) in serialized.part_relationships {
306 zip.start_file(&rels_path, options)?;
307 zip.write_all(&rels_xml)?;
308 }
309
310 for (name, data) in serialized.parts {
312 let zip_name = name.strip_prefix('/').unwrap_or(name);
314 zip.start_file(zip_name, options)?;
315 zip.write_all(data)?;
316 }
317
318 zip.finish()?;
319 Ok(())
320 }
321
322 #[cfg(feature = "agile-encryption")]
327 pub fn write_encrypted_to(&self, output: &mut Vec<u8>, password: &str) -> Result<()> {
328 let mut plaintext = std::io::Cursor::new(Vec::new());
329 self.write_to(&mut plaintext)?;
330 crate::encryption::write_encrypted_package(output, &plaintext.into_inner(), password)
331 }
332
333 pub fn get_part(&self, part_name: &str) -> Option<&[u8]> {
335 let key = deterministic_part_key(&self.parts, part_name)?;
336 self.parts.get(key).map(Vec::as_slice)
337 }
338
339 pub fn contains_part(&self, part_name: &str) -> bool {
341 deterministic_part_key(&self.parts, part_name).is_some()
342 }
343
344 #[cfg(feature = "digital-signatures")]
345 pub(crate) fn stored_part_name(&self, part_name: &str) -> Option<&str> {
346 deterministic_part_key(&self.parts, part_name).map(String::as_str)
347 }
348
349 pub fn set_part(&mut self, part_name: &str, data: Vec<u8>) {
351 let key = deterministic_part_key(&self.parts, part_name)
352 .cloned()
353 .unwrap_or_else(|| part_name.to_owned());
354 self.parts.insert(key, data);
355 }
356
357 pub fn remove_part(&mut self, part_name: &str) -> Option<Vec<u8>> {
359 let key = deterministic_part_key(&self.parts, part_name)?.clone();
360 self.parts.remove(&key)
361 }
362
363 pub(crate) fn content_types_bytes(&self) -> Result<Vec<u8>> {
364 self.content_types.validate_identities()?;
365 if let Some(source) = &self.content_types_source
366 && source.content_types == self.content_types
367 {
368 return Ok(source.xml.clone());
369 }
370 self.content_types.to_xml()
371 }
372
373 #[cfg(feature = "digital-signatures")]
379 pub fn verify_signatures(&self) -> Result<Vec<crate::SignatureReport>> {
380 crate::signature::verify_signatures(self)
381 }
382
383 #[cfg(feature = "digital-signatures")]
390 pub fn sign(
391 &mut self,
392 private_key_pkcs8_der: &[u8],
393 certificate_der: &[u8],
394 ) -> Result<crate::SignatureReport> {
395 let mut candidate = self.clone();
396 let report = crate::signature::create_signature(
397 &mut candidate,
398 private_key_pkcs8_der,
399 certificate_der,
400 )?;
401 *self = candidate;
402 Ok(report)
403 }
404
405 pub fn get_part_rels(&self, part_name: &str) -> Option<&Relationships> {
407 let key = deterministic_part_key(&self.part_rels, part_name)?;
408 self.part_rels.get(key)
409 }
410
411 pub fn get_part_rels_mut(&mut self, part_name: &str) -> Option<&mut Relationships> {
413 let key = deterministic_part_key(&self.part_rels, part_name)?.clone();
414 self.part_rels.get_mut(&key)
415 }
416
417 #[cfg(feature = "digital-signatures")]
418 pub(crate) fn stored_part_rels_owner(&self, part_name: &str) -> Option<&str> {
419 deterministic_part_key(&self.part_rels, part_name).map(String::as_str)
420 }
421
422 pub fn get_or_create_part_rels(&mut self, part_name: &str) -> &mut Relationships {
424 let key = deterministic_part_key(&self.part_rels, part_name)
425 .cloned()
426 .unwrap_or_else(|| part_name.to_owned());
427 self.part_rels.entry(key).or_default()
428 }
429
430 pub fn set_part_rels(&mut self, part_name: &str, relationships: Relationships) {
433 let key = deterministic_part_key(&self.part_rels, part_name)
434 .cloned()
435 .unwrap_or_else(|| part_name.to_owned());
436 self.part_rels.insert(key, relationships);
437 }
438
439 pub fn remove_part_rels(&mut self, part_name: &str) -> Option<Relationships> {
441 let key = deterministic_part_key(&self.part_rels, part_name)?.clone();
442 self.part_rels.remove(&key)
443 }
444
445 pub fn resolve_rel_target(source_part: &str, rel_target: &str) -> String {
452 let joined = if rel_target.starts_with('/') {
453 rel_target.to_string()
454 } else {
455 let dir = match source_part.rfind('/') {
457 Some(pos) => &source_part[..=pos],
458 None => "/",
459 };
460 format!("{dir}{rel_target}")
461 };
462 normalize_part_name(&joined)
463 }
464
465 pub fn main_document_part(&self) -> Option<String> {
467 self.package_rels
468 .get_by_type(rel_types::DOCUMENT)
469 .map(|rel| {
470 if rel.target.starts_with('/') {
471 rel.target.clone()
472 } else {
473 format!("/{}", rel.target)
474 }
475 })
476 }
477
478 pub fn new() -> Self {
480 OpcPackage {
481 content_types: ContentTypes::minimal(),
482 package_rels: Relationships::new(),
483 part_rels: HashMap::new(),
484 parts: HashMap::new(),
485 content_types_source: None,
486 malformed_source_entries: HashMap::new(),
487 }
488 }
489
490 pub fn with_main_part(part_name: &str, content_type: &str) -> Self {
492 let mut package = Self::new();
493 let part_name = part_name.strip_prefix('/').unwrap_or(part_name);
494 package.package_rels.add(rel_types::DOCUMENT, part_name);
495 package
496 .content_types
497 .add_override(&format!("/{part_name}"), content_type);
498 package
499 }
500
501 fn serialize(&self) -> Result<SerializedPackage<'_>> {
502 self.validate_graph()?;
503
504 let content_types = self.content_types_bytes()?;
505 let package_relationships = self.package_rels.to_xml()?;
506 let mut part_relationships = Vec::with_capacity(self.part_rels.len());
507 let mut relationship_owners = self.part_rels.iter().collect::<Vec<_>>();
508 relationship_owners.sort_by_key(|(owner, _)| owner.as_str());
509 for (owner, relationships) in relationship_owners {
510 part_relationships.push((part_name_to_rels_path(owner), relationships.to_xml()?));
511 }
512
513 let mut parts = self
514 .parts
515 .iter()
516 .map(|(name, data)| (name.as_str(), data.as_slice()))
517 .collect::<Vec<_>>();
518 parts.sort_by_key(|(name, _)| *name);
519 validate_zip_entry_identities(&part_relationships, &parts)?;
520
521 self.reject_written_xml_characters("[Content_Types].xml", &content_types)?;
524 self.reject_written_xml_characters("_rels/.rels", &package_relationships)?;
525 for (rels_path, rels_xml) in &part_relationships {
526 self.reject_written_xml_characters(rels_path, rels_xml)?;
527 }
528 for (name, data) in &parts {
529 if is_xml_entry(&self.content_types, name) {
530 self.reject_written_xml_characters(name, data)?;
531 }
532 }
533 Ok(SerializedPackage {
534 content_types,
535 package_relationships,
536 part_relationships,
537 parts,
538 })
539 }
540
541 pub(crate) fn validate_graph(&self) -> Result<()> {
542 validate_part_map_identities(&self.parts)?;
543 validate_part_map_identities(&self.part_rels)?;
544 self.content_types.validate_identities()?;
545 self.package_rels.validate_ids()?;
546 for relationships in self.part_rels.values() {
547 relationships.validate_ids()?;
548 }
549 Ok(())
550 }
551}
552
553impl OpcPackage {
554 fn reject_written_xml_characters(&self, entry: &str, data: &[u8]) -> Result<()> {
557 let Some((code_point, line, column)) = first_invalid_xml_character(data) else {
558 return Ok(());
559 };
560 let name = entry.strip_prefix('/').unwrap_or(entry);
561 if self.malformed_source_entries.get(name).map(Vec::as_slice) == Some(data) {
562 return Ok(());
563 }
564 Err(OpcError::InvalidXmlCharacter {
565 part: format!("/{name}"),
566 code_point,
567 line,
568 column,
569 })
570 }
571}
572
573fn is_xml_entry(content_types: &ContentTypes, name: &str) -> bool {
576 let lower = name.to_ascii_lowercase();
577 lower.ends_with(".rels")
578 || lower.ends_with("[content_types].xml")
579 || content_types
580 .content_type_for(&format!("/{}", name.strip_prefix('/').unwrap_or(name)))
581 .is_some_and(|content_type| content_type.to_ascii_lowercase().ends_with("xml"))
582}
583
584fn first_invalid_xml_character(data: &[u8]) -> Option<(u32, usize, usize)> {
588 if let Some(body) = data.strip_prefix(&[0xFF, 0xFE]) {
589 return first_invalid_utf16_character(body, true);
590 }
591 if let Some(body) = data.strip_prefix(&[0xFE, 0xFF]) {
592 return first_invalid_utf16_character(body, false);
593 }
594 if data.len() >= 4 && data[1] == 0 && data[3] == 0 {
595 return first_invalid_utf16_character(data, true);
596 }
597 if data.len() >= 4 && data[0] == 0 && data[2] == 0 {
598 return first_invalid_utf16_character(data, false);
599 }
600 let (offset, code_point) = data
601 .iter()
602 .enumerate()
603 .find_map(|(index, byte)| match byte {
604 0x00..=0x08 | 0x0B | 0x0C | 0x0E..=0x1F => Some((index, u32::from(*byte))),
605 0xEF if data.get(index + 1) == Some(&0xBF) => match data.get(index + 2) {
606 Some(0xBE) => Some((index, 0xFFFE)),
607 Some(0xBF) => Some((index, 0xFFFF)),
608 _ => None,
609 },
610 _ => None,
611 })?;
612 let body_start = if data.starts_with(&[0xEF, 0xBB, 0xBF]) {
613 3
614 } else {
615 0
616 };
617 let mut line = 1;
618 let mut line_start = body_start;
619 let mut index = body_start;
620 while index < offset {
621 match data[index] {
622 b'\r' => {
623 line += 1;
624 if data.get(index + 1) == Some(&b'\n') {
625 index += 1;
626 }
627 line_start = index + 1;
628 }
629 b'\n' => {
630 line += 1;
631 line_start = index + 1;
632 }
633 _ => {}
634 }
635 index += 1;
636 }
637 let column = data[line_start.min(offset)..offset]
638 .iter()
639 .filter(|byte| **byte & 0xC0 != 0x80)
640 .count()
641 + 1;
642 Some((code_point, line, column))
643}
644
645fn first_invalid_utf16_character(data: &[u8], little_endian: bool) -> Option<(u32, usize, usize)> {
646 let units = data.chunks_exact(2).map(|pair| {
647 if little_endian {
648 u16::from_le_bytes([pair[0], pair[1]])
649 } else {
650 u16::from_be_bytes([pair[0], pair[1]])
651 }
652 });
653 let mut line = 1;
654 let mut column = 1;
655 let mut after_cr = false;
656 for decoded in char::decode_utf16(units) {
657 let code_point = match decoded {
658 Ok(character) => u32::from(character),
659 Err(error) => u32::from(error.unpaired_surrogate()),
660 };
661 if matches!(code_point, 0x00..=0x08 | 0x0B | 0x0C | 0x0E..=0x1F | 0xD800..=0xDFFF | 0xFFFE | 0xFFFF)
662 {
663 return Some((code_point, line, column));
664 }
665 match code_point {
666 0x0D => {
667 line += 1;
668 column = 1;
669 after_cr = true;
670 }
671 0x0A if after_cr => after_cr = false,
672 0x0A => {
673 line += 1;
674 column = 1;
675 }
676 _ => {
677 column += 1;
678 after_cr = false;
679 }
680 }
681 }
682 None
683}
684
685pub(crate) fn part_identity(part_name: &str) -> String {
686 normalize_part_name(part_name).to_ascii_lowercase()
687}
688
689fn deterministic_part_key<'a, T>(
690 values: &'a HashMap<String, T>,
691 part_name: &str,
692) -> Option<&'a String> {
693 let identity = part_identity(part_name);
694 values
695 .keys()
696 .filter(|candidate| part_identity(candidate) == identity)
697 .min()
698}
699
700fn validate_part_map_identities<T>(values: &HashMap<String, T>) -> Result<()> {
701 let mut identities = HashSet::with_capacity(values.len());
702 let mut names = values.keys().collect::<Vec<_>>();
703 names.sort_unstable();
704 for name in names {
705 if !identities.insert(part_identity(name)) {
706 return Err(OpcError::DuplicatePartName(name.clone()));
707 }
708 }
709 Ok(())
710}
711
712fn validate_zip_entry_identities(
713 part_relationships: &[(String, Vec<u8>)],
714 parts: &[(&str, &[u8])],
715) -> Result<()> {
716 let mut identities = HashSet::from([
717 part_identity("/[Content_Types].xml"),
718 part_identity("/_rels/.rels"),
719 ]);
720 for (name, _) in part_relationships {
721 if !identities.insert(part_identity(name)) {
722 return Err(OpcError::DuplicatePartName(name.clone()));
723 }
724 }
725 for (name, _) in parts {
726 if !identities.insert(part_identity(name)) {
727 return Err(OpcError::DuplicatePartName((*name).to_owned()));
728 }
729 }
730 Ok(())
731}
732
733fn pre_index_entry_count<R: Read + Seek>(reader: &mut R) -> Result<u64> {
734 let file_len = reader.seek(SeekFrom::End(0))?;
735 if file_len < EOCD_LEN {
736 return Err(invalid_zip("end of central directory not found"));
737 }
738
739 let tail_len = file_len.min(ZIP_TAIL_LEN);
740 let tail_start = file_len - tail_len;
741 reader.seek(SeekFrom::Start(tail_start))?;
742 let mut tail = vec![0; tail_len as usize];
743 reader.read_exact(&mut tail)?;
744
745 let last_candidate = tail.len() - EOCD_LEN as usize;
746 let entry_count = (0..=last_candidate)
747 .rev()
748 .filter(|&offset| tail[offset..].starts_with(&EOCD_SIGNATURE))
749 .filter_map(|offset| eocd_entry_count(&tail, tail_start, file_len, offset))
750 .max();
751
752 entry_count.ok_or_else(|| invalid_zip("valid end of central directory not found"))
753}
754
755fn eocd_entry_count(tail: &[u8], tail_start: u64, file_len: u64, offset: usize) -> Option<u64> {
756 let eocd = tail.get(offset..offset.checked_add(EOCD_LEN as usize)?)?;
757 let comment_len = u64::from(read_u16(eocd, 20)?);
758 let absolute_offset = tail_start.checked_add(offset as u64)?;
759 if absolute_offset
760 .checked_add(EOCD_LEN)?
761 .checked_add(comment_len)?
762 != file_len
763 {
764 return None;
765 }
766
767 let disk_number = read_u16(eocd, 4)?;
768 let central_directory_disk = read_u16(eocd, 6)?;
769 if disk_number != 0 || central_directory_disk != 0 {
770 return None;
771 }
772
773 let entries_on_disk = read_u16(eocd, 8)?;
774 let total_entries = read_u16(eocd, 10)?;
775 let central_directory_size = read_u32(eocd, 12)?;
776 let central_directory_offset = read_u32(eocd, 16)?;
777 let uses_zip64 = entries_on_disk == u16::MAX
778 || total_entries == u16::MAX
779 || central_directory_size == u32::MAX
780 || central_directory_offset == u32::MAX;
781
782 if uses_zip64 {
783 return zip64_entry_count(tail, tail_start, absolute_offset);
784 }
785 if entries_on_disk != total_entries
786 || !central_directory_bounds_are_plausible(
787 absolute_offset,
788 u64::from(central_directory_offset),
789 u64::from(central_directory_size),
790 )
791 {
792 return None;
793 }
794
795 Some(u64::from(total_entries))
796}
797
798fn zip64_entry_count(tail: &[u8], tail_start: u64, eocd_offset: u64) -> Option<u64> {
799 let locator_offset = eocd_offset.checked_sub(ZIP64_EOCD_LOCATOR_LEN)?;
800 let locator = tail_slice(tail, tail_start, locator_offset, ZIP64_EOCD_LOCATOR_LEN)?;
801 if !locator.starts_with(&ZIP64_EOCD_LOCATOR_SIGNATURE)
802 || read_u32(locator, 4)? != 0
803 || read_u32(locator, 16)? != 1
804 {
805 return None;
806 }
807
808 let zip64_offset = read_u64(locator, 8)?;
809 let zip64 = tail_slice(tail, tail_start, zip64_offset, MIN_ZIP64_EOCD_LEN)?;
810 if !zip64.starts_with(&ZIP64_EOCD_SIGNATURE) {
811 return None;
812 }
813 let record_size = read_u64(zip64, 4)?;
814 if record_size < 44
815 || zip64_offset.checked_add(12)?.checked_add(record_size)? != locator_offset
816 || read_u32(zip64, 16)? != 0
817 || read_u32(zip64, 20)? != 0
818 {
819 return None;
820 }
821
822 let entries_on_disk = read_u64(zip64, 24)?;
823 let total_entries = read_u64(zip64, 32)?;
824 if entries_on_disk != total_entries
825 || !central_directory_bounds_are_plausible(
826 zip64_offset,
827 read_u64(zip64, 48)?,
828 read_u64(zip64, 40)?,
829 )
830 {
831 return None;
832 }
833
834 Some(total_entries)
835}
836
837fn central_directory_bounds_are_plausible(
838 directory_end: u64,
839 relative_directory_offset: u64,
840 directory_size: u64,
841) -> bool {
842 directory_end
843 .checked_sub(directory_size)
844 .is_some_and(|actual_directory_offset| relative_directory_offset <= actual_directory_offset)
845}
846
847fn tail_slice(tail: &[u8], tail_start: u64, absolute_offset: u64, len: u64) -> Option<&[u8]> {
848 let offset = usize::try_from(absolute_offset.checked_sub(tail_start)?).ok()?;
849 let len = usize::try_from(len).ok()?;
850 tail.get(offset..offset.checked_add(len)?)
851}
852
853fn read_u16(bytes: &[u8], offset: usize) -> Option<u16> {
854 Some(u16::from_le_bytes(
855 bytes.get(offset..offset.checked_add(2)?)?.try_into().ok()?,
856 ))
857}
858
859fn read_u32(bytes: &[u8], offset: usize) -> Option<u32> {
860 Some(u32::from_le_bytes(
861 bytes.get(offset..offset.checked_add(4)?)?.try_into().ok()?,
862 ))
863}
864
865fn read_u64(bytes: &[u8], offset: usize) -> Option<u64> {
866 Some(u64::from_le_bytes(
867 bytes.get(offset..offset.checked_add(8)?)?.try_into().ok()?,
868 ))
869}
870
871fn invalid_zip(detail: &'static str) -> OpcError {
872 OpcError::Zip(zip::result::ZipError::InvalidArchive(detail.into()))
873}
874
875impl Default for OpcPackage {
876 fn default() -> Self {
877 Self::new()
878 }
879}
880
881pub fn write_atomic_file(
911 path: &Path,
912 bytes: &[u8],
913 staging_tag: &str,
914 invalid_name_message: &'static str,
915 exhausted_message: &'static str,
916) -> std::io::Result<()> {
917 if std::fs::metadata(path).is_ok_and(|metadata| !metadata.is_file() && !metadata.is_dir()) {
920 return std::fs::write(path, bytes);
921 }
922 let path = resolve_symbolic_links(path)?;
923 let parent = path.parent().unwrap_or_else(|| Path::new("."));
924 let file_name = path.file_name().ok_or_else(|| {
925 std::io::Error::new(std::io::ErrorKind::InvalidInput, invalid_name_message)
926 })?;
927 let existing = std::fs::metadata(&path).ok();
928 if existing.as_ref().is_some_and(std::fs::Metadata::is_file) {
933 std::fs::OpenOptions::new().write(true).open(&path)?;
934 }
935 let permissions = existing
938 .filter(|_| cfg!(unix))
939 .map(|metadata| metadata.permissions());
940 let mut options = std::fs::OpenOptions::new();
941 options.write(true).create_new(true);
942 #[cfg(unix)]
946 if let Some(permissions) = &permissions {
947 use std::os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _};
948 options.mode(permissions.mode());
949 }
950 let process_id = if cfg!(target_family = "wasm") {
953 0
954 } else {
955 std::process::id()
956 };
957 for attempt in 0..128_u8 {
958 let temporary = parent.join(staging_file_name(
959 file_name,
960 &format!(".{staging_tag}-{process_id}-{attempt}.tmp"),
961 ));
962 let mut file = match options.open(&temporary) {
963 Ok(file) => file,
964 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
965 Err(error) => return Err(error),
966 };
967 let result = permissions
968 .clone()
969 .map_or(Ok(()), |permissions| file.set_permissions(permissions))
970 .and_then(|()| file.write_all(bytes))
971 .and_then(|()| file.sync_all());
972 drop(file);
973 let result = result.and_then(|()| replace_file(&temporary, &path));
974 if result.is_err() {
975 let _ = std::fs::remove_file(&temporary);
976 }
977 return result;
978 }
979 Err(std::io::Error::new(
980 std::io::ErrorKind::AlreadyExists,
981 exhausted_message,
982 ))
983}
984
985fn staging_file_name(file_name: &std::ffi::OsStr, suffix: &str) -> std::ffi::OsString {
990 let available = 255_usize.saturating_sub(1 + suffix.len());
991 let mut name = std::ffi::OsString::from(".");
992 if file_name.len() <= available {
993 name.push(file_name);
994 } else {
995 let file_name = file_name.to_string_lossy();
996 let mut end = available.min(file_name.len());
997 while !file_name.is_char_boundary(end) {
998 end -= 1;
999 }
1000 name.push(&file_name[..end]);
1001 }
1002 name.push(suffix);
1003 name
1004}
1005
1006fn resolve_symbolic_links(path: &Path) -> std::io::Result<PathBuf> {
1011 let mut resolved = path.to_path_buf();
1012 for _ in 0..=40 {
1015 match std::fs::symlink_metadata(&resolved) {
1016 Ok(metadata) if metadata.file_type().is_symlink() => {
1017 let target = std::fs::read_link(&resolved)?;
1018 resolved = match resolved.parent() {
1019 Some(parent) => parent.join(target),
1020 None => target,
1021 };
1022 }
1023 _ => return Ok(resolved),
1024 }
1025 }
1026 Err(std::io::Error::new(
1027 std::io::ErrorKind::InvalidInput,
1028 "too many levels of symbolic links",
1029 ))
1030}
1031
1032#[cfg(not(target_os = "windows"))]
1033fn replace_file(source: &Path, destination: &Path) -> std::io::Result<()> {
1034 std::fs::rename(source, destination)
1035}
1036
1037#[cfg(target_os = "windows")]
1038fn replace_file(source: &Path, destination: &Path) -> std::io::Result<()> {
1039 use std::os::windows::ffi::OsStrExt;
1040
1041 const MOVEFILE_REPLACE_EXISTING: u32 = 0x1;
1042 const MOVEFILE_WRITE_THROUGH: u32 = 0x8;
1043
1044 #[link(name = "kernel32")]
1045 unsafe extern "system" {
1046 fn MoveFileExW(
1047 existing_file_name: *const u16,
1048 new_file_name: *const u16,
1049 flags: u32,
1050 ) -> i32;
1051 }
1052
1053 let source: Vec<u16> = source.as_os_str().encode_wide().chain(Some(0)).collect();
1054 let destination: Vec<u16> = destination
1055 .as_os_str()
1056 .encode_wide()
1057 .chain(Some(0))
1058 .collect();
1059 let replaced = unsafe {
1061 MoveFileExW(
1062 source.as_ptr(),
1063 destination.as_ptr(),
1064 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
1065 )
1066 };
1067 if replaced == 0 {
1068 Err(std::io::Error::last_os_error())
1069 } else {
1070 Ok(())
1071 }
1072}
1073
1074#[cfg(test)]
1075fn docx_package() -> OpcPackage {
1076 let mut package = OpcPackage::with_main_part(
1077 "word/document.xml",
1078 "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
1079 );
1080 package.content_types.add_override(
1081 "/word/styles.xml",
1082 "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml",
1083 );
1084 package
1085}
1086
1087fn normalize_part_name(path: &str) -> String {
1092 let mut segments: Vec<&str> = Vec::new();
1093 for segment in path.split('/') {
1094 match segment {
1095 "" | "." => {}
1096 ".." => {
1097 segments.pop();
1098 }
1099 other => segments.push(other),
1100 }
1101 }
1102
1103 let mut out = String::with_capacity(path.len());
1104 for segment in segments {
1105 out.push('/');
1106 out.push_str(segment);
1107 }
1108 if out.is_empty() { "/".to_string() } else { out }
1109}
1110
1111fn rels_path_to_part_name(rels_path: &str) -> String {
1114 let lowercase = rels_path.to_ascii_lowercase();
1118 let without_suffix = if lowercase.ends_with(".rels") {
1119 &rels_path[..rels_path.len() - ".rels".len()]
1120 } else {
1121 rels_path
1122 };
1123 let lowercase = without_suffix.to_ascii_lowercase();
1124 let path = match lowercase.rfind("_rels/") {
1125 Some(pos) => format!(
1126 "{}{}",
1127 &without_suffix[..pos],
1128 &without_suffix[pos + "_rels/".len()..]
1129 ),
1130 None => without_suffix.to_string(),
1131 };
1132 if path.starts_with('/') {
1133 path
1134 } else {
1135 format!("/{path}")
1136 }
1137}
1138
1139fn part_name_to_rels_path(part_name: &str) -> String {
1142 let name = part_name.strip_prefix('/').unwrap_or(part_name);
1143 if let Some(pos) = name.rfind('/') {
1144 let dir = &name[..pos];
1145 let file = &name[pos + 1..];
1146 format!("{dir}/_rels/{file}.rels")
1147 } else {
1148 format!("_rels/{name}.rels")
1149 }
1150}
1151
1152#[cfg(test)]
1153mod tests {
1154 use super::*;
1155
1156 fn package_zip(entries: &[(&str, &[u8])]) -> std::io::Cursor<Vec<u8>> {
1157 package_zip_with_comment(entries, &[])
1158 }
1159
1160 fn package_zip_with_comment(
1161 entries: &[(&str, &[u8])],
1162 comment: &[u8],
1163 ) -> std::io::Cursor<Vec<u8>> {
1164 let mut buffer = std::io::Cursor::new(Vec::new());
1165 {
1166 let mut zip = ZipWriter::new(&mut buffer);
1167 let options = SimpleFileOptions::default();
1168 for (name, data) in entries {
1169 zip.start_file(name, options).unwrap();
1170 zip.write_all(data).unwrap();
1171 }
1172 zip.set_raw_comment(comment.to_vec().into_boxed_slice())
1173 .unwrap();
1174 zip.finish().unwrap();
1175 }
1176 buffer.set_position(0);
1177 buffer
1178 }
1179
1180 fn replace_zip_name(bytes: &mut [u8], old: &[u8], raw: &[u8], utf8: bool) {
1181 assert_eq!(old.len(), raw.len());
1182 let mut changed = 0;
1183 for offset in 0..bytes.len().saturating_sub(46) {
1184 let (header, flags, length) = match &bytes[offset..offset + 4] {
1185 b"PK\x03\x04" => (30, 6, 26),
1186 b"PK\x01\x02" => (46, 8, 28),
1187 _ => continue,
1188 };
1189 let name_len = u16::from_le_bytes(
1190 bytes[offset + length..offset + length + 2]
1191 .try_into()
1192 .unwrap(),
1193 ) as usize;
1194 let start = offset + header;
1195 if name_len != old.len() || bytes.get(start..start + name_len) != Some(old) {
1196 continue;
1197 }
1198 bytes[start..start + name_len].copy_from_slice(raw);
1199 let mut value = u16::from_le_bytes(
1200 bytes[offset + flags..offset + flags + 2]
1201 .try_into()
1202 .unwrap(),
1203 );
1204 value = (value & !(1 << 11)) | if utf8 { 1 << 11 } else { 0 };
1205 bytes[offset + flags..offset + flags + 2].copy_from_slice(&value.to_le_bytes());
1206 changed += 1;
1207 }
1208 assert_eq!(changed, 2, "both local and central names must be patched");
1209 }
1210
1211 #[test]
1212 fn zip9_utf8_and_cp437_names_preserve_opaque_payloads() {
1213 for (raw, utf8) in [
1214 (b"x\xc3\xa9.bin".as_slice(), false),
1215 (b"x\xc3\xa9.bin".as_slice(), true),
1216 (b"x\x82.bin".as_slice(), false),
1217 (b"x\x82.bin".as_slice(), true),
1218 ] {
1219 let name = format!("{}.bin", "a".repeat(raw.len() - 4));
1220 let mut bytes = package_zip(&[
1221 ("[Content_Types].xml", br#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="bin" ContentType="application/octet-stream"/></Types>"#),
1222 (&name, b"opaque\0\xff"),
1223 ]).into_inner();
1224 replace_zip_name(&mut bytes, name.as_bytes(), raw, utf8);
1225 let before = bytes.clone();
1226 let package = OpcPackage::from_reader(std::io::Cursor::new(&bytes)).unwrap();
1227 assert_eq!(
1228 package.get_part("/xé.bin"),
1229 Some(b"opaque\0\xff".as_slice())
1230 );
1231 let mut saved = std::io::Cursor::new(Vec::new());
1232 package.write_to(&mut saved).unwrap();
1233 let reopened =
1234 OpcPackage::from_reader(std::io::Cursor::new(saved.into_inner())).unwrap();
1235 assert_eq!(
1236 reopened.get_part("/xé.bin"),
1237 Some(b"opaque\0\xff".as_slice())
1238 );
1239 assert_eq!(bytes, before);
1240 }
1241 }
1242
1243 #[test]
1244 fn zip9_decoded_aliases_reach_existing_duplicate_rejection() {
1245 let mut bytes = package_zip(&[
1246 (
1247 "[Content_Types].xml",
1248 br#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"/>"#,
1249 ),
1250 ("aa.bin", b"first"),
1251 ("aaa.bin", b"second"),
1252 ])
1253 .into_inner();
1254 replace_zip_name(&mut bytes, b"aa.bin", b"x\x82.bin", false);
1255 replace_zip_name(&mut bytes, b"aaa.bin", b"x\xc3\xa9.bin", false);
1256 let before = bytes.clone();
1257 assert!(
1258 matches!(OpcPackage::from_reader(std::io::Cursor::new(&bytes)),
1259 Err(OpcError::DuplicatePartName(name)) if name == "xé.bin")
1260 );
1261 assert_eq!(bytes, before);
1262 }
1263
1264 #[test]
1265 fn zip9_ordinary_writer_header_is_deterministic_version_twenty() {
1266 let mut package = OpcPackage::new();
1267 package.set_part("opaque.bin", b"opaque\0\xff".to_vec());
1268 let mut first = std::io::Cursor::new(Vec::new());
1269 let mut second = std::io::Cursor::new(Vec::new());
1270 package.write_to(&mut first).unwrap();
1271 package.write_to(&mut second).unwrap();
1272 assert_eq!(first.get_ref(), second.get_ref());
1273 let bytes = first.get_ref();
1274 let mut central = 0;
1275 for offset in 0..bytes.len().saturating_sub(46) {
1276 if &bytes[offset..offset + 4] != b"PK\x01\x02" {
1277 continue;
1278 }
1279 let read =
1280 |at| u16::from_le_bytes(bytes[offset + at..offset + at + 2].try_into().unwrap());
1281 assert_eq!(read(4) & 0xff, 20);
1282 assert_eq!(read(6), 20);
1283 assert_eq!(read(12), 0);
1284 assert_eq!(read(14), 33);
1285 assert_eq!(read(10), 8);
1286 central += 1;
1287 }
1288 assert_eq!(central, 3);
1289 let reopened = OpcPackage::from_reader(std::io::Cursor::new(bytes)).unwrap();
1290 assert_eq!(
1291 reopened.get_part("opaque.bin"),
1292 Some(b"opaque\0\xff".as_slice())
1293 );
1294 }
1295
1296 fn fake_eocd(total_entries: u16) -> [u8; EOCD_LEN as usize] {
1297 let mut eocd = [0; EOCD_LEN as usize];
1298 eocd[..4].copy_from_slice(&EOCD_SIGNATURE);
1299 eocd[8..10].copy_from_slice(&total_entries.to_le_bytes());
1300 eocd[10..12].copy_from_slice(&total_entries.to_le_bytes());
1301 eocd
1302 }
1303
1304 const MINIMAL_CONTENT_TYPES: &[u8] =
1305 br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1306<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
1307 <Default Extension="xml" ContentType="application/xml"/>
1308</Types>"#;
1309
1310 #[test]
1311 fn unchanged_case_variant_content_types_preserve_producer_bytes_and_order() {
1312 const PRODUCER_CONTENT_TYPES: &[u8] = br#"<?xml version="1.0"?><ct:Types xmlns:ct="http://schemas.openxmlformats.org/package/2006/content-types"><ct:Override ContentType="application/example+xml" PartName="/WORD/DOCUMENT.XML"/><ct:Default ContentType="image/png" Extension="PNG"/><ct:Default ContentType="application/xml" Extension="XML"/></ct:Types>"#;
1313 let package = OpcPackage::from_reader(package_zip(&[
1314 ("[Content_Types].xml", PRODUCER_CONTENT_TYPES),
1315 ("word/document.xml", b"<document/>"),
1316 ]))
1317 .unwrap();
1318 assert_eq!(
1319 package.content_types.content_type_for("/word/document.xml"),
1320 Some("application/example+xml")
1321 );
1322 assert_eq!(
1323 package.content_types.content_type_for("/word/image.png"),
1324 Some("image/png")
1325 );
1326
1327 let mut output = std::io::Cursor::new(Vec::new());
1328 package.write_to(&mut output).unwrap();
1329 output.set_position(0);
1330 let mut archive = ZipArchive::new(output).unwrap();
1331 let mut saved = Vec::new();
1332 archive
1333 .by_name("[Content_Types].xml")
1334 .unwrap()
1335 .read_to_end(&mut saved)
1336 .unwrap();
1337 assert_eq!(saved, PRODUCER_CONTENT_TYPES);
1338 }
1339
1340 #[test]
1341 fn package_write_rejects_direct_content_type_case_conflicts() {
1342 let mut package = OpcPackage::new();
1343 package
1344 .content_types
1345 .defaults
1346 .insert("PNG".to_string(), "image/first".to_string());
1347 package
1348 .content_types
1349 .defaults
1350 .insert("png".to_string(), "image/second".to_string());
1351 let mut output = std::io::Cursor::new(Vec::new());
1352
1353 assert!(matches!(
1354 package.write_to(&mut output),
1355 Err(OpcError::InvalidContentTypes)
1356 ));
1357 assert!(output.into_inner().is_empty());
1358 }
1359
1360 #[test]
1361 fn part_access_is_case_insensitive_and_preserves_first_spelling() {
1362 let mut package = OpcPackage::new();
1363 package.set_part("/WORD/DOCUMENT.XML", b"first".to_vec());
1364 package.set_part("word/document.xml", b"second".to_vec());
1365 assert_eq!(package.parts.len(), 1);
1366 assert!(package.parts.contains_key("/WORD/DOCUMENT.XML"));
1367 assert!(package.contains_part("/word/./document.xml"));
1368 assert_eq!(package.get_part("word/document.xml"), Some(&b"second"[..]));
1369
1370 package
1371 .get_or_create_part_rels("/WORD/DOCUMENT.XML")
1372 .add("urn:test", "target.xml");
1373 assert_eq!(package.part_rels.len(), 1);
1374 assert!(
1375 package
1376 .get_part_rels("word/document.xml")
1377 .is_some_and(|relationships| relationships.items.len() == 1)
1378 );
1379 assert!(package.remove_part_rels("/word/document.xml").is_some());
1380 assert_eq!(
1381 package.remove_part("/word/document.xml"),
1382 Some(b"second".to_vec())
1383 );
1384 }
1385
1386 #[test]
1387 fn direct_case_conflicts_and_duplicate_relationship_ids_fail_before_output() {
1388 let mut package = OpcPackage::new();
1389 package
1390 .parts
1391 .insert("/WORD/DOCUMENT.XML".to_owned(), b"first".to_vec());
1392 package
1393 .parts
1394 .insert("/word/document.xml".to_owned(), b"second".to_vec());
1395 let mut output = std::io::Cursor::new(Vec::new());
1396 assert!(matches!(
1397 package.write_to(&mut output),
1398 Err(OpcError::DuplicatePartName(_))
1399 ));
1400 assert!(output.into_inner().is_empty());
1401
1402 let mut package = OpcPackage::new();
1403 for target in ["first.xml", "second.xml"] {
1404 package.package_rels.items.push(crate::Relationship {
1405 id: "producer-id".to_owned(),
1406 rel_type: "urn:test".to_owned(),
1407 target: target.to_owned(),
1408 target_mode: None,
1409 });
1410 }
1411 let mut output = std::io::Cursor::new(Vec::new());
1412 assert!(matches!(
1413 package.write_to(&mut output),
1414 Err(OpcError::InvalidRelationship)
1415 ));
1416 assert!(output.into_inner().is_empty());
1417 }
1418
1419 #[test]
1420 fn save_validates_before_truncating_destination() {
1421 let destination = std::env::temp_dir().join(format!(
1422 "rdocx-opc-save-sentinel-{}-{}",
1423 std::process::id(),
1424 std::thread::current().name().unwrap_or("test")
1425 ));
1426 std::fs::write(&destination, b"sentinel").unwrap();
1427
1428 let mut package = OpcPackage::new();
1429 for target in ["first.xml", "second.xml"] {
1430 package.package_rels.items.push(crate::Relationship {
1431 id: "duplicate".to_owned(),
1432 rel_type: "urn:test".to_owned(),
1433 target: target.to_owned(),
1434 target_mode: None,
1435 });
1436 }
1437 assert!(matches!(
1438 package.save(&destination),
1439 Err(OpcError::InvalidRelationship)
1440 ));
1441 assert_eq!(std::fs::read(&destination).unwrap(), b"sentinel");
1442 std::fs::remove_file(destination).unwrap();
1443 }
1444
1445 fn save_test_directory(label: &str) -> PathBuf {
1446 let directory =
1447 std::env::temp_dir().join(format!("oxml-opc-save-{label}-{}", std::process::id()));
1448 let _ = std::fs::remove_dir_all(&directory);
1449 std::fs::create_dir_all(&directory).unwrap();
1450 directory
1451 }
1452
1453 fn staging_files(directory: &Path) -> Vec<PathBuf> {
1454 std::fs::read_dir(directory)
1455 .unwrap()
1456 .map(|entry| entry.unwrap().path())
1457 .filter(|path| path.extension().is_some_and(|extension| extension == "tmp"))
1458 .collect()
1459 }
1460
1461 fn package_bytes(package: &OpcPackage) -> Vec<u8> {
1462 let mut output = std::io::Cursor::new(Vec::new());
1463 package.write_to(&mut output).unwrap();
1464 output.into_inner()
1465 }
1466
1467 #[cfg(unix)]
1468 #[test]
1469 fn save_replaces_an_existing_file_by_rename_and_keeps_its_mode() {
1470 use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _};
1471
1472 let directory = save_test_directory("rename");
1473 let destination = directory.join("existing.docx");
1474 std::fs::write(&destination, b"previous bytes").unwrap();
1475 std::fs::set_permissions(&destination, std::fs::Permissions::from_mode(0o600)).unwrap();
1476 let previous_inode = std::fs::metadata(&destination).unwrap().ino();
1477
1478 let package = docx_package();
1479 package.save(&destination).unwrap();
1480
1481 let metadata = std::fs::metadata(&destination).unwrap();
1483 assert_ne!(metadata.ino(), previous_inode);
1484 assert_eq!(metadata.permissions().mode() & 0o7777, 0o600);
1485 assert_eq!(
1486 std::fs::read(&destination).unwrap(),
1487 package_bytes(&package)
1488 );
1489 assert!(staging_files(&directory).is_empty());
1490 std::fs::remove_dir_all(directory).unwrap();
1491 }
1492
1493 #[cfg(unix)]
1494 #[test]
1495 fn save_through_a_symbolic_link_replaces_the_file_it_names() {
1496 use std::os::unix::fs::PermissionsExt as _;
1497
1498 let directory = save_test_directory("symlink");
1499 let links = directory.join("links");
1500 std::fs::create_dir(&links).unwrap();
1501 let target = directory.join("target.docx");
1502 std::fs::write(&target, b"previous bytes").unwrap();
1503 std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o640)).unwrap();
1504 let link = links.join("link.docx");
1505 std::os::unix::fs::symlink("../target.docx", &link).unwrap();
1506
1507 let package = docx_package();
1508 package.save(&link).unwrap();
1509
1510 assert!(
1511 std::fs::symlink_metadata(&link)
1512 .unwrap()
1513 .file_type()
1514 .is_symlink()
1515 );
1516 assert_eq!(
1517 std::fs::read_link(&link).unwrap(),
1518 Path::new("../target.docx")
1519 );
1520 assert_eq!(std::fs::read(&target).unwrap(), package_bytes(&package));
1521 assert_eq!(
1522 std::fs::metadata(&target).unwrap().permissions().mode() & 0o7777,
1523 0o640
1524 );
1525
1526 std::fs::remove_file(&target).unwrap();
1528 package.save(&link).unwrap();
1529 assert!(
1530 std::fs::symlink_metadata(&link)
1531 .unwrap()
1532 .file_type()
1533 .is_symlink()
1534 );
1535 assert_eq!(std::fs::read(&target).unwrap(), package_bytes(&package));
1536
1537 std::fs::remove_file(&target).unwrap();
1539 let mut head = target.clone();
1540 for index in 0..40 {
1541 let chained = links.join(format!("chain-{index}.docx"));
1542 std::os::unix::fs::symlink(&head, &chained).unwrap();
1543 head = chained;
1544 }
1545 package.save(&head).unwrap();
1546 assert_eq!(std::fs::read(&target).unwrap(), package_bytes(&package));
1547 let too_long = links.join("chain-40.docx");
1548 std::os::unix::fs::symlink(&head, &too_long).unwrap();
1549 assert!(package.save(&too_long).is_err());
1550 assert!(staging_files(&directory).is_empty());
1551 assert!(staging_files(&links).is_empty());
1552 std::fs::remove_dir_all(directory).unwrap();
1553 }
1554
1555 #[test]
1556 fn failed_save_keeps_the_destination_and_leaves_no_staging_file() {
1557 let directory = save_test_directory("failure");
1558
1559 let occupied = directory.join("directory.docx");
1561 std::fs::create_dir(&occupied).unwrap();
1562 std::fs::write(occupied.join("inner.xml"), b"inner bytes").unwrap();
1563 assert!(docx_package().save(&occupied).is_err());
1564 assert_eq!(
1565 std::fs::read(occupied.join("inner.xml")).unwrap(),
1566 b"inner bytes"
1567 );
1568 assert!(staging_files(&directory).is_empty());
1569
1570 let destination = directory.join("existing.docx");
1572 std::fs::write(&destination, b"previous bytes").unwrap();
1573 for attempt in 0..128_u8 {
1574 let staging = directory.join(format!(
1575 ".existing.docx.oxml-opc-{}-{attempt}.tmp",
1576 std::process::id()
1577 ));
1578 std::fs::write(staging, b"occupied").unwrap();
1579 }
1580 let error = docx_package().save(&destination).unwrap_err();
1581 assert!(
1582 error
1583 .to_string()
1584 .contains("could not allocate package save staging file"),
1585 "{error}"
1586 );
1587 assert_eq!(std::fs::read(&destination).unwrap(), b"previous bytes");
1588 assert_eq!(staging_files(&directory).len(), 128);
1589 std::fs::remove_dir_all(directory).unwrap();
1590 }
1591
1592 #[cfg(unix)]
1593 #[test]
1594 fn save_to_a_device_or_fifo_writes_in_place_and_keeps_the_node() {
1595 use std::os::unix::fs::FileTypeExt as _;
1596
1597 let package = docx_package();
1598 package.save("/dev/null").unwrap();
1599 assert!(
1600 std::fs::metadata("/dev/null")
1601 .unwrap()
1602 .file_type()
1603 .is_char_device()
1604 );
1605
1606 let directory = save_test_directory("fifo");
1607 let fifo = directory.join("pipe.docx");
1608 let status = std::process::Command::new("mkfifo")
1609 .arg(&fifo)
1610 .status()
1611 .unwrap();
1612 assert!(status.success());
1613 let reader = {
1614 let fifo = fifo.clone();
1615 std::thread::spawn(move || std::fs::read(fifo).unwrap())
1616 };
1617 package.save(&fifo).unwrap();
1618 assert!(
1621 std::fs::symlink_metadata(&fifo)
1622 .unwrap()
1623 .file_type()
1624 .is_fifo()
1625 );
1626 assert_eq!(reader.join().unwrap(), package_bytes(&package));
1627 assert!(staging_files(&directory).is_empty());
1628 std::fs::remove_dir_all(directory).unwrap();
1629 }
1630
1631 #[cfg(unix)]
1632 #[test]
1633 fn save_refuses_a_read_only_file_and_leaves_it_as_it_was() {
1634 use std::os::unix::fs::PermissionsExt as _;
1635
1636 let directory = save_test_directory("read-only");
1637 let destination = directory.join("protected.docx");
1638 std::fs::write(&destination, b"previous bytes").unwrap();
1639 for mode in [0o444, 0o464] {
1642 std::fs::set_permissions(&destination, std::fs::Permissions::from_mode(mode)).unwrap();
1643 if std::fs::OpenOptions::new()
1645 .write(true)
1646 .open(&destination)
1647 .is_ok()
1648 {
1649 continue;
1650 }
1651 let error = docx_package().save(&destination).unwrap_err();
1652 assert!(
1653 matches!(&error, OpcError::Io(error) if error.kind() == std::io::ErrorKind::PermissionDenied),
1654 "{mode:o}: {error}"
1655 );
1656 assert_eq!(std::fs::read(&destination).unwrap(), b"previous bytes");
1657 }
1658 assert!(staging_files(&directory).is_empty());
1659 std::fs::remove_dir_all(directory).unwrap();
1660 }
1661
1662 #[cfg(unix)]
1665 #[test]
1666 fn save_shortens_the_staging_name_of_a_long_file_name() {
1667 let directory = save_test_directory("long-name");
1668 let destination =
1672 directory.join(format!("{}{}.docx", "a".repeat(220), "\u{6587}".repeat(10)));
1673 let package = docx_package();
1674 package.save(&destination).unwrap();
1675 std::fs::write(&destination, b"previous bytes").unwrap();
1676 package.save(&destination).unwrap();
1677 assert_eq!(
1678 std::fs::read(&destination).unwrap(),
1679 package_bytes(&package)
1680 );
1681 assert!(staging_files(&directory).is_empty());
1682 std::fs::remove_dir_all(directory).unwrap();
1683 }
1684
1685 #[test]
1686 fn bounded_reader_rejects_too_many_entries() {
1687 let archive = package_zip(&[
1688 ("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
1689 ("word/document.xml", b"<document/>"),
1690 ]);
1691 let error = OpcPackage::from_reader_with_limits(
1692 archive,
1693 PackageReadLimits {
1694 max_entries: 1,
1695 max_part_uncompressed_bytes: 1_024,
1696 max_total_uncompressed_bytes: 2_048,
1697 },
1698 )
1699 .unwrap_err();
1700
1701 assert!(matches!(
1702 error,
1703 OpcError::PackageLimitExceeded {
1704 kind: "entry count",
1705 limit: 1
1706 }
1707 ));
1708 }
1709
1710 #[test]
1711 fn later_eocd_signature_in_comment_cannot_hide_real_entry_count() {
1712 let mut archive = package_zip_with_comment(
1713 &[
1714 ("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
1715 ("word/document.xml", b"<document/>"),
1716 ],
1717 &fake_eocd(1),
1718 );
1719
1720 assert_eq!(pre_index_entry_count(&mut archive).unwrap(), 2);
1721 }
1722
1723 #[test]
1724 fn truncated_zip64_metadata_returns_an_error_without_panicking() {
1725 let mut bytes = vec![0; 4 + ZIP64_EOCD_LOCATOR_LEN as usize + EOCD_LEN as usize];
1726 bytes[..4].copy_from_slice(&ZIP64_EOCD_SIGNATURE);
1727
1728 let locator_offset = 4;
1729 bytes[locator_offset..locator_offset + 4].copy_from_slice(&ZIP64_EOCD_LOCATOR_SIGNATURE);
1730 bytes[locator_offset + 8..locator_offset + 16].copy_from_slice(&0_u64.to_le_bytes());
1731 bytes[locator_offset + 16..locator_offset + 20].copy_from_slice(&1_u32.to_le_bytes());
1732
1733 let eocd_offset = locator_offset + ZIP64_EOCD_LOCATOR_LEN as usize;
1734 bytes[eocd_offset..eocd_offset + 4].copy_from_slice(&EOCD_SIGNATURE);
1735 bytes[eocd_offset + 8..eocd_offset + 12].fill(0xff);
1736 bytes[eocd_offset + 12..eocd_offset + 20].fill(0xff);
1737
1738 let error = pre_index_entry_count(&mut std::io::Cursor::new(bytes)).unwrap_err();
1739 assert!(matches!(error, OpcError::Zip(_)));
1740 }
1741
1742 #[test]
1743 fn bounded_reader_rejects_oversized_parts_and_totals() {
1744 let entries = [
1745 ("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
1746 ("word/document.xml", b"<document/>".as_slice()),
1747 ];
1748 let part_error = OpcPackage::from_reader_with_limits(
1749 package_zip(&entries),
1750 PackageReadLimits {
1751 max_entries: 8,
1752 max_part_uncompressed_bytes: 16,
1753 max_total_uncompressed_bytes: 1_024,
1754 },
1755 )
1756 .unwrap_err();
1757 assert!(matches!(
1758 part_error,
1759 OpcError::PackageLimitExceeded {
1760 kind: "part size",
1761 limit: 16
1762 }
1763 ));
1764
1765 let total_error = OpcPackage::from_reader_with_limits(
1766 package_zip(&entries),
1767 PackageReadLimits {
1768 max_entries: 8,
1769 max_part_uncompressed_bytes: 1_024,
1770 max_total_uncompressed_bytes: MINIMAL_CONTENT_TYPES.len() as u64,
1771 },
1772 )
1773 .unwrap_err();
1774 assert!(matches!(
1775 total_error,
1776 OpcError::PackageLimitExceeded {
1777 kind: "total uncompressed size",
1778 ..
1779 }
1780 ));
1781 }
1782
1783 fn independently_built_pptx() -> std::io::Cursor<Vec<u8>> {
1784 const CONTENT_TYPES: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
1785<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
1786 <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
1787 <Default Extension="xml" ContentType="application/xml"/>
1788 <Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
1789 <Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
1790 <Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>
1791 <Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>
1792 <Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
1793</Types>"#;
1794 const PACKAGE_RELS: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
1795<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
1796 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>
1797</Relationships>"#;
1798 const PRESENTATION_RELS: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
1799<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
1800 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>
1801 <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
1802</Relationships>"#;
1803 const SLIDE_RELS: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
1804<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
1805 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
1806</Relationships>"#;
1807 const SLIDE_LAYOUT_RELS: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
1808<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
1809 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>
1810</Relationships>"#;
1811 const SLIDE_MASTER_RELS: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
1812<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
1813 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
1814 <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>
1815</Relationships>"#;
1816 const PRESENTATION: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
1817<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
1818 <p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>
1819 <p:sldIdLst><p:sldId id="256" r:id="rId2"/></p:sldIdLst>
1820 <p:sldSz cx="12192000" cy="6858000"/>
1821 <p:notesSz cx="6858000" cy="9144000"/>
1822</p:presentation>"#;
1823 const SLIDE: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
1824<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
1825 <p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld>
1826 <p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>
1827</p:sld>"#;
1828 const SLIDE_LAYOUT: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
1829<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
1830 <p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld>
1831 <p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>
1832</p:sldLayout>"#;
1833 const SLIDE_MASTER: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
1834<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">
1835 <p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld>
1836 <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"/>
1837 <p:sldLayoutIdLst><p:sldLayoutId id="2147483648" r:id="rId1"/></p:sldLayoutIdLst>
1838 <p:txStyles><p:titleStyle/><p:bodyStyle/><p:otherStyle/></p:txStyles>
1839</p:sldMaster>"#;
1840 const THEME: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
1841<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Minimal">
1842 <a:themeElements>
1843 <a:clrScheme name="Minimal">
1844 <a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
1845 <a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
1846 <a:dk2><a:srgbClr val="1F497D"/></a:dk2>
1847 <a:lt2><a:srgbClr val="EEECE1"/></a:lt2>
1848 <a:accent1><a:srgbClr val="4F81BD"/></a:accent1>
1849 <a:accent2><a:srgbClr val="C0504D"/></a:accent2>
1850 <a:accent3><a:srgbClr val="9BBB59"/></a:accent3>
1851 <a:accent4><a:srgbClr val="8064A2"/></a:accent4>
1852 <a:accent5><a:srgbClr val="4BACC6"/></a:accent5>
1853 <a:accent6><a:srgbClr val="F79646"/></a:accent6>
1854 <a:hlink><a:srgbClr val="0000FF"/></a:hlink>
1855 <a:folHlink><a:srgbClr val="800080"/></a:folHlink>
1856 </a:clrScheme>
1857 <a:fontScheme name="Minimal">
1858 <a:majorFont><a:latin typeface="Arial"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>
1859 <a:minorFont><a:latin typeface="Arial"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>
1860 </a:fontScheme>
1861 <a:fmtScheme name="Minimal">
1862 <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>
1863 <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>
1864 <a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst>
1865 <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>
1866 </a:fmtScheme>
1867 </a:themeElements>
1868</a:theme>"#;
1869
1870 package_zip(&[
1871 ("[Content_Types].xml", CONTENT_TYPES),
1872 ("_rels/.rels", PACKAGE_RELS),
1873 ("ppt/presentation.xml", PRESENTATION),
1874 ("ppt/_rels/presentation.xml.rels", PRESENTATION_RELS),
1875 ("ppt/slides/slide1.xml", SLIDE),
1876 ("ppt/slides/_rels/slide1.xml.rels", SLIDE_RELS),
1877 ("ppt/slideLayouts/slideLayout1.xml", SLIDE_LAYOUT),
1878 (
1879 "ppt/slideLayouts/_rels/slideLayout1.xml.rels",
1880 SLIDE_LAYOUT_RELS,
1881 ),
1882 ("ppt/slideMasters/slideMaster1.xml", SLIDE_MASTER),
1883 (
1884 "ppt/slideMasters/_rels/slideMaster1.xml.rels",
1885 SLIDE_MASTER_RELS,
1886 ),
1887 ("ppt/theme/theme1.xml", THEME),
1888 ])
1889 }
1890
1891 fn pptx_package() -> OpcPackage {
1892 let mut package =
1893 OpcPackage::with_main_part("ppt/presentation.xml", crate::content_types::PRESENTATION);
1894 package
1895 .content_types
1896 .add_override("/ppt/slides/slide1.xml", crate::content_types::SLIDE);
1897 package.content_types.add_override(
1898 "/ppt/slideLayouts/slideLayout1.xml",
1899 crate::content_types::SLIDE_LAYOUT,
1900 );
1901 package.set_part("/ppt/presentation.xml", b"<p:presentation/>".to_vec());
1902 package.set_part("/ppt/slides/slide1.xml", b"<p:sld/>".to_vec());
1903 package.set_part(
1904 "/ppt/slideLayouts/slideLayout1.xml",
1905 b"<p:sldLayout/>".to_vec(),
1906 );
1907 package
1908 .get_or_create_part_rels("/ppt/presentation.xml")
1909 .add(rel_types::SLIDE, "slides/slide1.xml");
1910 package
1911 .get_or_create_part_rels("/ppt/slides/slide1.xml")
1912 .add(rel_types::SLIDE_LAYOUT, "../slideLayouts/slideLayout1.xml");
1913 package
1914 }
1915
1916 #[test]
1917 fn a_part_holding_a_character_xml_cannot_carry_is_not_written() {
1918 let mut package = OpcPackage::with_main_part(
1919 "ppt/presentation.xml",
1920 "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml",
1921 );
1922 package
1923 .content_types
1924 .add_default("bin", "application/octet-stream");
1925 package.set_part("/ppt/presentation.xml", b"<p:presentation/>".to_vec());
1926 package.set_part("/ppt/data.bin", vec![0, 1, 0x0b, 0xff]);
1927 let utf16 = "<a/>"
1928 .encode_utf16()
1929 .flat_map(u16::to_le_bytes)
1930 .collect::<Vec<_>>();
1931 package.set_part("/customXml/item1.xml", [&[0xFF, 0xFE][..], &utf16].concat());
1932 let utf16_big_endian = "<a/>"
1933 .encode_utf16()
1934 .flat_map(u16::to_be_bytes)
1935 .collect::<Vec<_>>();
1936 package.set_part("/customXml/item2.xml", utf16_big_endian.clone());
1937 let mut buffer = std::io::Cursor::new(Vec::new());
1938 package
1939 .write_to(&mut buffer)
1940 .expect("binary and valid UTF-16 XML remain writable");
1941 for (data, point) in [
1942 (
1943 "<a>\u{1}</a>"
1944 .encode_utf16()
1945 .flat_map(u16::to_be_bytes)
1946 .collect::<Vec<_>>(),
1947 1,
1948 ),
1949 (
1950 [
1951 &[0xFF, 0xFE][..],
1952 &"<a>\u{ffff}</a>"
1953 .encode_utf16()
1954 .flat_map(u16::to_le_bytes)
1955 .collect::<Vec<_>>(),
1956 ]
1957 .concat(),
1958 0xFFFF,
1959 ),
1960 ] {
1961 package.set_part("/customXml/item2.xml", data);
1962 assert!(matches!(
1963 package.write_to(std::io::Cursor::new(Vec::new())),
1964 Err(OpcError::InvalidXmlCharacter { part, code_point, line: 1, column: 4 })
1965 if part == "/customXml/item2.xml" && code_point == point
1966 ));
1967 }
1968 package.set_part("/customXml/item2.xml", utf16_big_endian);
1969
1970 for (text, code_point, line, column) in [
1971 ("<p:presentation>\n <a>x\u{1}</a>", 0x1, 2, 7),
1972 ("<p:presentation name=\"\u{b}\"/>", 0xB, 1, 23),
1973 ("<p:presentation>\u{ffff}</p:presentation>", 0xFFFF, 1, 17),
1974 ("\u{feff}<p:presentation>\r\n\r<a>\u{1}</a>", 0x1, 3, 4),
1975 ("<p:presentation>\r<é>\u{c}</é>", 0xC, 2, 4),
1976 ] {
1977 package.set_part("/ppt/presentation.xml", text.as_bytes().to_vec());
1978 let error = package
1979 .write_to(std::io::Cursor::new(Vec::new()))
1980 .expect_err("a forbidden character must not be written");
1981 assert!(
1982 matches!(
1983 &error,
1984 OpcError::InvalidXmlCharacter { part, code_point: found, line: l, column: c }
1985 if part == "/ppt/presentation.xml"
1986 && *found == code_point
1987 && *l == line
1988 && *c == column
1989 ),
1990 "{error}"
1991 );
1992 }
1993 assert_eq!(
1994 package
1995 .write_to(std::io::Cursor::new(Vec::new()))
1996 .unwrap_err()
1997 .to_string(),
1998 "/ppt/presentation.xml holds U+000C at line 2, column 4, a character XML 1.0 cannot carry"
1999 );
2000
2001 package.set_part("/ppt/presentation.xml", b"<p:presentation/>".to_vec());
2002 package
2003 .get_or_create_part_rels("/ppt/presentation.xml")
2004 .add("urn:type", "target\u{1}.xml");
2005 assert_eq!(
2006 package
2007 .write_to(std::io::Cursor::new(Vec::new()))
2008 .unwrap_err()
2009 .to_string(),
2010 "/ppt/_rels/presentation.xml.rels holds U+0001 at line 3, column 57, a character XML 1.0 cannot carry"
2011 );
2012 package.part_rels.clear();
2013
2014 let producer = "<p:presentation>producer\u{1}</p:presentation>".as_bytes();
2017 package.set_part("/ppt/presentation.xml", b"<p:presentation/>".to_vec());
2018 let mut valid = std::io::Cursor::new(Vec::new());
2019 package.write_to(&mut valid).unwrap();
2020 let mut source = zip::ZipArchive::new(std::io::Cursor::new(valid.into_inner())).unwrap();
2021 let mut rebuilt = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
2022 for index in 0..source.len() {
2023 let mut entry = source.by_index(index).unwrap();
2024 let name = entry.name().unwrap().into_owned();
2025 let mut data = Vec::new();
2026 std::io::Read::read_to_end(&mut entry, &mut data).unwrap();
2027 if name == "ppt/presentation.xml" {
2028 data = producer.to_vec();
2029 }
2030 rebuilt
2031 .start_file(name, SimpleFileOptions::default())
2032 .unwrap();
2033 rebuilt.write_all(&data).unwrap();
2034 }
2035 let source = rebuilt.finish().unwrap().into_inner();
2036 let mut reopened = OpcPackage::from_reader(std::io::Cursor::new(source)).unwrap();
2037 let mut saved = std::io::Cursor::new(Vec::new());
2038 reopened
2039 .write_to(&mut saved)
2040 .expect("producer bytes pass through");
2041 saved.set_position(0);
2042 assert_eq!(
2043 OpcPackage::from_reader(saved)
2044 .unwrap()
2045 .get_part("/ppt/presentation.xml"),
2046 Some(producer)
2047 );
2048 reopened.set_part(
2049 "/ppt/presentation.xml",
2050 "<p:presentation>written\u{1}</p:presentation>"
2051 .as_bytes()
2052 .to_vec(),
2053 );
2054 assert!(matches!(
2055 reopened.write_to(std::io::Cursor::new(Vec::new())),
2056 Err(OpcError::InvalidXmlCharacter { code_point: 1, .. })
2057 ));
2058 }
2059
2060 #[test]
2061 fn with_main_part_resolves_and_round_trips() {
2062 let mut package = OpcPackage::with_main_part(
2063 "ppt/presentation.xml",
2064 "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml",
2065 );
2066 package.set_part("/ppt/presentation.xml", b"<p:presentation/>".to_vec());
2067
2068 assert_eq!(
2069 package.main_document_part().as_deref(),
2070 Some("/ppt/presentation.xml")
2071 );
2072 assert_eq!(
2073 package
2074 .content_types
2075 .content_type_for("/ppt/presentation.xml"),
2076 Some(
2077 "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"
2078 )
2079 );
2080
2081 let mut buffer = std::io::Cursor::new(Vec::new());
2082 package.write_to(&mut buffer).unwrap();
2083 buffer.set_position(0);
2084 let round_tripped = OpcPackage::from_reader(buffer).unwrap();
2085
2086 assert_eq!(
2087 round_tripped.main_document_part().as_deref(),
2088 Some("/ppt/presentation.xml")
2089 );
2090 assert_eq!(
2091 round_tripped.get_part("/ppt/presentation.xml"),
2092 Some(b"<p:presentation/>".as_slice())
2093 );
2094 }
2095
2096 #[test]
2097 fn pptx_package_resolves_main_slide_and_layout_parts() {
2098 let package = pptx_package();
2099 let mut buffer = std::io::Cursor::new(Vec::new());
2100 package.write_to(&mut buffer).unwrap();
2101 buffer.set_position(0);
2102 let reopened = OpcPackage::from_reader(buffer).unwrap();
2103
2104 let presentation_part = reopened.main_document_part().unwrap();
2105 assert_eq!(presentation_part, "/ppt/presentation.xml");
2106
2107 let slide_target = &reopened
2108 .get_part_rels(&presentation_part)
2109 .unwrap()
2110 .get_by_type(rel_types::SLIDE)
2111 .unwrap()
2112 .target;
2113 let slide_part = OpcPackage::resolve_rel_target(&presentation_part, slide_target);
2114 assert_eq!(slide_part, "/ppt/slides/slide1.xml");
2115
2116 let layout_target = &reopened
2117 .get_part_rels(&slide_part)
2118 .unwrap()
2119 .get_by_type(rel_types::SLIDE_LAYOUT)
2120 .unwrap()
2121 .target;
2122 let layout_part = OpcPackage::resolve_rel_target(&slide_part, layout_target);
2123 assert_eq!(layout_part, "/ppt/slideLayouts/slideLayout1.xml");
2124
2125 assert!(reopened.parts.contains_key(&presentation_part));
2126 assert!(reopened.parts.contains_key(&slide_part));
2127 assert!(reopened.parts.contains_key(&layout_part));
2128 }
2129
2130 #[test]
2131 fn independently_built_pptx_opens_and_resolves_relationships() {
2132 let package = OpcPackage::from_reader(independently_built_pptx()).unwrap();
2133
2134 let presentation_part = package.main_document_part().unwrap();
2135 assert_eq!(presentation_part, "/ppt/presentation.xml");
2136
2137 let presentation_master_target = &package
2138 .get_part_rels(&presentation_part)
2139 .unwrap()
2140 .get_by_type(rel_types::SLIDE_MASTER)
2141 .unwrap()
2142 .target;
2143 let presentation_master_part =
2144 OpcPackage::resolve_rel_target(&presentation_part, presentation_master_target);
2145 assert_eq!(
2146 presentation_master_part,
2147 "/ppt/slideMasters/slideMaster1.xml"
2148 );
2149
2150 let slide_target = &package
2151 .get_part_rels(&presentation_part)
2152 .unwrap()
2153 .get_by_type(rel_types::SLIDE)
2154 .unwrap()
2155 .target;
2156 let slide_part = OpcPackage::resolve_rel_target(&presentation_part, slide_target);
2157 assert_eq!(slide_part, "/ppt/slides/slide1.xml");
2158
2159 let layout_target = &package
2160 .get_part_rels(&slide_part)
2161 .unwrap()
2162 .get_by_type(rel_types::SLIDE_LAYOUT)
2163 .unwrap()
2164 .target;
2165 let layout_part = OpcPackage::resolve_rel_target(&slide_part, layout_target);
2166 assert_eq!(layout_part, "/ppt/slideLayouts/slideLayout1.xml");
2167
2168 let master_target = &package
2169 .get_part_rels(&layout_part)
2170 .unwrap()
2171 .get_by_type(rel_types::SLIDE_MASTER)
2172 .unwrap()
2173 .target;
2174 let master_part = OpcPackage::resolve_rel_target(&layout_part, master_target);
2175 assert_eq!(master_part, "/ppt/slideMasters/slideMaster1.xml");
2176 assert_eq!(master_part, presentation_master_part);
2177
2178 let master_layout_target = &package
2179 .get_part_rels(&master_part)
2180 .unwrap()
2181 .get_by_type(rel_types::SLIDE_LAYOUT)
2182 .unwrap()
2183 .target;
2184 let master_layout_part = OpcPackage::resolve_rel_target(&master_part, master_layout_target);
2185 assert_eq!(master_layout_part, layout_part);
2186
2187 let theme_target = &package
2188 .get_part_rels(&master_part)
2189 .unwrap()
2190 .get_by_type(rel_types::THEME)
2191 .unwrap()
2192 .target;
2193 let theme_part = OpcPackage::resolve_rel_target(&master_part, theme_target);
2194 assert_eq!(theme_part, "/ppt/theme/theme1.xml");
2195
2196 let master_xml = std::str::from_utf8(package.parts.get(&master_part).unwrap()).unwrap();
2197 let layout_id = master_xml
2198 .split_once("<p:sldLayoutId id=\"")
2199 .unwrap()
2200 .1
2201 .split_once('"')
2202 .unwrap()
2203 .0
2204 .parse::<u64>()
2205 .unwrap();
2206 assert!(layout_id >= 2_147_483_648);
2207
2208 assert!(package.parts.contains_key(&presentation_part));
2209 assert!(package.parts.contains_key(&slide_part));
2210 assert!(package.parts.contains_key(&layout_part));
2211 assert!(package.parts.contains_key(&master_part));
2212 assert!(package.parts.contains_key(&theme_part));
2213 }
2214
2215 #[test]
2216 fn presentation_layout_target_resolves_one_directory_up() {
2217 assert_eq!(
2218 OpcPackage::resolve_rel_target(
2219 "/ppt/slides/slide1.xml",
2220 "../slideLayouts/slideLayout1.xml"
2221 ),
2222 "/ppt/slideLayouts/slideLayout1.xml"
2223 );
2224 }
2225
2226 #[test]
2227 fn rels_path_conversion() {
2228 assert_eq!(
2229 rels_path_to_part_name("word/_rels/document.xml.rels"),
2230 "/word/document.xml"
2231 );
2232 assert_eq!(
2233 part_name_to_rels_path("/word/document.xml"),
2234 "word/_rels/document.xml.rels"
2235 );
2236 }
2237
2238 #[test]
2239 fn resolve_relative_target() {
2240 assert_eq!(
2241 OpcPackage::resolve_rel_target("/word/document.xml", "styles.xml"),
2242 "/word/styles.xml"
2243 );
2244 assert_eq!(
2245 OpcPackage::resolve_rel_target("/word/document.xml", "/word/styles.xml"),
2246 "/word/styles.xml"
2247 );
2248 }
2249
2250 #[test]
2251 fn resolve_target_collapses_parent_segments() {
2252 assert_eq!(
2254 OpcPackage::resolve_rel_target("/word/charts/chart1.xml", "../media/image1.png"),
2255 "/word/media/image1.png"
2256 );
2257 assert_eq!(
2258 OpcPackage::resolve_rel_target("/word/document.xml", "./styles.xml"),
2259 "/word/styles.xml"
2260 );
2261 assert_eq!(
2262 OpcPackage::resolve_rel_target("/word/document.xml", "../../../etc/passwd"),
2263 "/etc/passwd"
2264 );
2265 }
2266
2267 #[test]
2268 fn zip_entry_that_escapes_root_is_clamped_to_root() {
2269 let archive = package_zip(&[
2270 ("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
2271 ("../../etc/passwd", b"not a real password file"),
2272 ]);
2273
2274 let package = OpcPackage::from_reader(archive).unwrap();
2275
2276 assert_eq!(
2277 package.get_part("/etc/passwd"),
2278 Some(b"not a real password file".as_slice())
2279 );
2280 assert!(package.parts.keys().all(|name| !name.contains("..")));
2281 }
2282
2283 #[test]
2284 fn duplicate_normalized_part_names_are_rejected() {
2285 for alias in [
2286 "word/./document.xml",
2287 "word//document.xml",
2288 "WORD/DOCUMENT.XML",
2289 ] {
2290 let archive = package_zip(&[
2291 ("[Content_Types].xml", MINIMAL_CONTENT_TYPES),
2292 ("word/document.xml", b"first"),
2293 (alias, b"second"),
2294 ]);
2295
2296 let error = OpcPackage::from_reader(archive).unwrap_err();
2297 assert!(matches!(
2298 error,
2299 OpcError::DuplicatePartName(name) if name.eq_ignore_ascii_case("word/document.xml")
2300 ));
2301 }
2302 }
2303
2304 #[test]
2305 fn loader_resolves_case_equivalent_special_parts_and_relationship_owners() {
2306 let content_types = br#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Override PartName="/WORD/DOCUMENT.XML" ContentType="application/xml"/></Types>"#;
2307 let package_relationships = format!(
2308 r#"<Relationships xmlns="{}"><Relationship Id="rId1" Type="{}" Target="WORD/DOCUMENT.XML"/></Relationships>"#,
2309 "http://schemas.openxmlformats.org/package/2006/relationships",
2310 rel_types::DOCUMENT
2311 );
2312 let part_relationships = format!(
2313 r#"<Relationships xmlns="{}"><Relationship Id="rId1" Type="urn:test" Target="TARGET.XML"/></Relationships>"#,
2314 "http://schemas.openxmlformats.org/package/2006/relationships"
2315 );
2316 let package = OpcPackage::from_reader(package_zip(&[
2317 ("[CONTENT_TYPES].XML", content_types),
2318 ("_RELS/.RELS", package_relationships.as_bytes()),
2319 ("WORD/DOCUMENT.XML", b"<document/>"),
2320 (
2321 "WORD/_RELS/DOCUMENT.XML.RELS",
2322 part_relationships.as_bytes(),
2323 ),
2324 ]))
2325 .unwrap();
2326
2327 assert_eq!(
2328 package.get_part("/word/document.xml"),
2329 Some(&b"<document/>"[..])
2330 );
2331 assert_eq!(package.package_rels.items.len(), 1);
2332 assert_eq!(
2333 package
2334 .get_part_rels("/word/document.xml")
2335 .unwrap()
2336 .items
2337 .len(),
2338 1
2339 );
2340 assert!(package.parts.contains_key("/WORD/DOCUMENT.XML"));
2341 }
2342
2343 #[test]
2344 fn absolute_zip_entry_is_normalized_to_package_root() {
2345 let archive = package_zip(&[
2346 ("/[Content_Types].xml", MINIMAL_CONTENT_TYPES),
2347 ("/absolute/path.xml", b"<absolute/>"),
2348 ]);
2349
2350 let package = OpcPackage::from_reader(archive).unwrap();
2351
2352 assert_eq!(
2353 package.get_part("/absolute/path.xml"),
2354 Some(b"<absolute/>".as_slice())
2355 );
2356 assert_eq!(
2357 package
2358 .parts
2359 .keys()
2360 .filter(|name| name.as_str() == "/absolute/path.xml")
2361 .count(),
2362 1
2363 );
2364 }
2365
2366 #[test]
2367 fn rels_path_suffix_is_stripped_once() {
2368 assert_eq!(
2369 rels_path_to_part_name("word/_rels/document.xml.rels"),
2370 "/word/document.xml"
2371 );
2372 assert_eq!(
2374 rels_path_to_part_name("word/_rels/odd.rels.rels"),
2375 "/word/odd.rels"
2376 );
2377 }
2378
2379 #[test]
2380 fn saved_packages_are_byte_identical() {
2381 let mut pkg = docx_package();
2382 for i in 0..40 {
2383 pkg.set_part(&format!("/word/media/image{i}.png"), vec![i as u8]);
2384 }
2385 pkg.get_or_create_part_rels("/word/document.xml")
2386 .add(rel_types::STYLES, "styles.xml");
2387
2388 let write = || {
2389 let mut buf = std::io::Cursor::new(Vec::new());
2390 pkg.write_to(&mut buf).unwrap();
2391 buf.into_inner()
2392 };
2393 assert_eq!(write(), write());
2394 }
2395
2396 #[test]
2397 fn new_docx_package() {
2398 let pkg = docx_package();
2399 assert!(pkg.main_document_part().is_some());
2400 assert_eq!(pkg.main_document_part().unwrap(), "/word/document.xml");
2401 }
2402
2403 #[test]
2404 fn round_trip_package() {
2405 let mut pkg = docx_package();
2406 pkg.set_part("/word/document.xml", b"<document/>".to_vec());
2407
2408 let mut buf = std::io::Cursor::new(Vec::new());
2410 pkg.write_to(&mut buf).unwrap();
2411
2412 buf.set_position(0);
2414 let pkg2 = OpcPackage::from_reader(buf).unwrap();
2415 assert_eq!(
2416 pkg2.get_part("/word/document.xml"),
2417 Some(b"<document/>".as_slice())
2418 );
2419 assert!(pkg2.main_document_part().is_some());
2420 }
2421}