Skip to main content

oxml_opc/
package.rs

1//! OPC Package reader and writer for ZIP-based OOXML packages.
2
3use 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/// A single part within the OPC package.
26#[derive(Debug, Clone)]
27pub struct PackagePart {
28    /// Part name (URI), e.g. "/word/document.xml"
29    pub name: String,
30    /// Raw bytes of the part content
31    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/// Resource limits applied while expanding an OPC ZIP archive.
48#[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/// An in-memory representation of an OPC package (ZIP archive).
64#[derive(Debug, Clone)]
65pub struct OpcPackage {
66    /// Content types from `[Content_Types].xml`
67    pub content_types: ContentTypes,
68    /// Package-level relationships from `_rels/.rels`
69    pub package_rels: Relationships,
70    /// Part-level relationships keyed by the part they belong to.
71    /// Key is the part name (e.g. "/word/document.xml"),
72    /// value is the parsed relationships.
73    pub part_rels: HashMap<String, Relationships>,
74    /// All parts keyed by their URI (e.g. "/word/document.xml").
75    pub parts: HashMap<String, Vec<u8>>,
76    content_types_source: Option<ContentTypesSource>,
77    /// The bytes of each XML entry that was read already holding a character
78    /// XML 1.0 cannot carry, keyed by ZIP entry name. Such producer bytes
79    /// are written back verbatim, while no writer can add such a character.
80    malformed_source_entries: HashMap<String, Vec<u8>>,
81}
82
83impl OpcPackage {
84    /// Open an OPC package from a file path.
85    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    /// Open an OPC package from any reader that implements Read + Seek.
91    pub fn from_reader<R: Read + Seek>(reader: R) -> Result<Self> {
92        Self::from_reader_with_limits(reader, PackageReadLimits::UNBOUNDED)
93    }
94
95    /// Open an agile-encrypted OPC package with no archive-expansion limit.
96    #[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    /// Authenticate and open an agile-encrypted OPC package with expansion limits.
102    #[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    /// Open an OPC package while bounding archive expansion.
113    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        // Read all entries from the ZIP
139        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        // Parse [Content_Types].xml
190        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        // Parse package-level relationships: _rels/.rels
196        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        // Parse all part-level .rels files
205        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                // Convert rels path to the part name it belongs to.
219                // e.g. "word/_rels/document.xml.rels" → "/word/document.xml"
220                let part_name = rels_path_to_part_name(&rels_path);
221                part_rels.insert(part_name, rels);
222            }
223        }
224
225        // Build parts map with leading "/" normalized
226        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    /// Save the OPC package to a file path.
264    ///
265    /// The complete ZIP is built in memory and published through
266    /// [`write_atomic_file`], so a failed save leaves an existing file as it
267    /// was.
268    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    /// Write the OPC package to any writer.
282    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        // Write [Content_Types].xml
295        zip.start_file("[Content_Types].xml", options)?;
296        zip.write_all(&serialized.content_types)?;
297
298        // Write _rels/.rels
299        zip.start_file("_rels/.rels", options)?;
300        zip.write_all(&serialized.package_relationships)?;
301
302        // Write part-level .rels files. Both loops iterate in sorted order so
303        // that saving the same package twice produces byte-identical output;
304        // a HashMap's order would vary between runs.
305        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        // Write all parts
311        for (name, data) in serialized.parts {
312            // Strip leading "/" for ZIP entry name
313            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    /// Append a password-protected CFB envelope around the deterministic OPC ZIP.
323    ///
324    /// The output buffer is unchanged if package serialization, encryption, or
325    /// allocation fails.
326    #[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    /// Get raw bytes of a part by its URI.
334    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    /// Return whether a case-equivalent normalized part name exists.
340    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    /// Set (or replace) a part's raw bytes.
350    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    /// Remove and return a case-equivalent normalized part.
358    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    /// Verify every digital signature discovered through the OPC relationship graph.
374    ///
375    /// Cryptographic validity authenticates the embedded certificate's key. It does
376    /// not establish that the certificate chains to a trusted root. Certificate
377    /// trust remains caller policy.
378    #[cfg(feature = "digital-signatures")]
379    pub fn verify_signatures(&self) -> Result<Vec<crate::SignatureReport>> {
380        crate::signature::verify_signatures(self)
381    }
382
383    /// Sign a cloned candidate package with the strict RSA-SHA256 profile.
384    ///
385    /// The private key must be PKCS#8 DER or one unencrypted PRIVATE KEY PEM
386    /// block. The certificate must be X.509 DER or one CERTIFICATE PEM block. The live package changes only after the candidate verifies with
387    /// complete declared coverage. Certificate-chain trust remains caller
388    /// policy.
389    #[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    /// Get the relationships for a specific part.
406    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    /// Get mutable relationships for a case-equivalent normalized owner.
412    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    /// Get or create the relationships for a specific part.
423    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    /// Replaces the relationships for a part while preserving the package's
431    /// existing spelling for a case-equivalent relationship owner.
432    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    /// Remove and return relationships for a case-equivalent normalized owner.
440    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    /// Resolve the target URI of a relationship relative to its source part.
446    ///
447    /// `.` and `..` segments are collapsed, so a target such as
448    /// `../media/image1.png` on `/word/charts/chart1.xml` resolves to
449    /// `/media/image1.png` and matches the key it is stored under. Without
450    /// this, parts referenced through a parent directory could never be found.
451    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            // Directory of the source part, including the trailing slash.
456            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    /// Find the main document part URI by looking at package relationships.
466    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    /// Create an empty package with the universal OPC content types.
479    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    /// Create a package whose office document relationship targets a main part.
491    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        // No writer may publish a character XML 1.0 forbids, so every XML
522        // entry is checked here, where every save of every format passes.
523        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    /// Refuses an entry to be written that holds a character XML 1.0 cannot
555    /// carry, unless it is the exact bytes a producer wrote into the source.
556    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
573/// Whether an entry is XML: a relationship or content types entry, or a part
574/// whose content type ends in `xml`.
575fn 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
584/// The code point, line and column of the first character outside the XML
585/// 1.0 `Char` production in a UTF-8 or UTF-16 entry. The byte scan keeps
586/// producer XML available even when its decoder would reject that character.
587fn 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
881/// Replace the file at `path` with `bytes` through a synced sibling file.
882///
883/// The bytes are written to `.{file name}.{staging_tag}-{process id}-{attempt}.tmp`
884/// beside the file being replaced, synced, and renamed over it, so readers see
885/// the old file or the new one and never a partial write. A file name too long
886/// for that staging name is shortened in it. Any failure removes the staged
887/// file and leaves the destination as it was. A symbolic link at `path` is
888/// followed and kept, and the file it names is replaced. On Unix the
889/// replacement keeps the permission bits of the file it replaces, from the
890/// moment the staged file is created. The owner, extended attributes, and
891/// other hard links of that file are not carried over. A file this process
892/// cannot open for writing is refused and left as it was.
893///
894/// A path that names a device or a FIFO, such as `/dev/null`, has no file to
895/// replace. It takes the bytes in place, as a plain write would.
896///
897/// `staging_tag` is a fragment of a file name and must not contain a path
898/// separator.
899///
900/// # Errors
901///
902/// Returns `invalid_name_message` when `path` names no file, the error of
903/// opening the file to replace for writing, such as
904/// [`PermissionDenied`](std::io::ErrorKind::PermissionDenied) when it is
905/// read-only, an
906/// [`InvalidInput`](std::io::ErrorKind::InvalidInput) error when `path` goes
907/// through more than 40 symbolic links, `exhausted_message` when every staging
908/// name is taken, and the I/O error of a failed write, staging, sync, or
909/// rename.
910pub 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    // Renaming over a device or a FIFO would put a regular file in its place.
918    // The kernel follows every link here, including /dev/stdout.
919    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    // A rename needs write access to the directory, not to the file it
929    // replaces. Opening the file for writing, without truncating it, refuses
930    // the files the in-place write refused, such as a read-only file or
931    // another user's file.
932    if existing.as_ref().is_some_and(std::fs::Metadata::is_file) {
933        std::fs::OpenOptions::new().write(true).open(&path)?;
934    }
935    // Only Unix mode bits carry over. On Windows the staged file keeps its
936    // default attributes.
937    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    // Creating the staged file with the kept mode means it is never more open
943    // than the file it replaces. The umask can narrow that mode, so it is set
944    // again once the file exists.
945    #[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    // wasm targets have no process id, and std panics when asked for one. The
951    // retry below covers two processes that share a staging name.
952    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
985/// Name a staged file `.{file name}{suffix}`, keeping it within 255 bytes.
986///
987/// Common file systems refuse a longer name, so a long file name keeps only
988/// the prefix that fits, cut on a character boundary.
989fn 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
1006/// Follow symbolic links from `path` to the file a save replaces.
1007///
1008/// A relative link target resolves against the directory holding the link,
1009/// and a dangling link resolves to the missing file it names.
1010fn resolve_symbolic_links(path: &Path) -> std::io::Result<PathBuf> {
1011    let mut resolved = path.to_path_buf();
1012    // Up to 40 links are followed, the Linux limit, so 41 paths are checked.
1013    // A longer chain is treated as a loop.
1014    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    // SAFETY: both path buffers are NUL-terminated and remain alive for the call.
1060    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
1087/// Collapse `.` and `..` segments in an absolute part name.
1088///
1089/// A `..` that would escape the package root is dropped, matching how OPC
1090/// consumers treat over-long parent traversals.
1091fn 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
1111/// Convert a .rels file path to the part name it belongs to.
1112/// e.g. "word/_rels/document.xml.rels" → "/word/document.xml"
1113fn rels_path_to_part_name(rels_path: &str) -> String {
1114    // Strip the ".rels" suffix once (`trim_end_matches` would strip repeats,
1115    // turning "a.rels.rels" into "a") and drop only the "_rels" path segment
1116    // that directly precedes the file name.
1117    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
1139/// Convert a part name to its .rels file path.
1140/// e.g. "/word/document.xml" → "word/_rels/document.xml.rels"
1141fn 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        // A new inode shows the file was replaced by rename, not rewritten in place.
1482        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        // A dangling link names the file the save creates.
1527        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        // A chain of 40 links is followed, and one more link is refused as a loop.
1538        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        // Renaming the staged file over a directory fails after staging.
1560        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        // Exhausted staging names fail before the destination is touched.
1571        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        // Checked before the join, so a save that replaced the FIFO fails here
1619        // instead of leaving the test waiting on a reader that never gets data.
1620        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        // 0o464 leaves write bits for the group but none for the owner, who
1640        // cannot write the file either.
1641        for mode in [0o444, 0o464] {
1642            std::fs::set_permissions(&destination, std::fs::Permissions::from_mode(mode)).unwrap();
1643            // Root writes a read-only file in place, so it may replace it too.
1644            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    // Unix only: on Windows the full path passes MAX_PATH, and the MoveFileExW
1663    // rename takes it without the `\\?\` prefix that std adds for long paths.
1664    #[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        // A name of 255 bytes in 235 characters fills the limit of ext4 and APFS.
1669        // The shortened staging name ends inside the run of three-byte
1670        // characters, so it has to cut on a character boundary.
1671        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        // A producer part that already held one is written back verbatim,
2015        // and only a writer's change to it is refused.
2016        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        // Charts and headers routinely reference media through a parent dir.
2253        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        // A part whose own name ends in ".rels" must keep that segment.
2373        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        // Write to memory
2409        let mut buf = std::io::Cursor::new(Vec::new());
2410        pkg.write_to(&mut buf).unwrap();
2411
2412        // Read back
2413        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}