Skip to main content

odox_core/
package.rs

1//! The zip container an `OpenDocument` document is: its entries, in the order
2//! they were stored, and the rules about the first one.
3//
4// Author: David M. Anderson
5// Built with AI assistance (Claude, Anthropic)
6
7use std::io::{Cursor, Read, Write};
8
9use zip::write::SimpleFileOptions;
10use zip::{CompressionMethod, ZipArchive, ZipWriter};
11
12use crate::Error;
13use crate::xml::{self, Element, Ns};
14
15/// One entry of a package.
16pub struct Part {
17    /// The entry's path inside the package, such as `content.xml` or
18    /// `Pictures/100002010000...png`.
19    pub name: String,
20    /// The entry's bytes, decompressed.
21    pub data: Vec<u8>,
22    /// Whether the entry was stored rather than deflated.
23    ///
24    /// Kept so that writing a package it read does not recompress a picture
25    /// that was already compressed when it went in, which costs time and grows
26    /// the file.
27    pub stored: bool,
28    /// Whether the entry is a directory rather than a file.
29    ///
30    /// An office application writes a few of these — `Configurations2/` and
31    /// `Thumbnails/` among them — and a package that comes back without them is
32    /// a package that has been changed. They carry no bytes.
33    pub directory: bool,
34}
35
36/// An `OpenDocument` package: a zip archive whose first entry declares the media
37/// type of everything else in it.
38///
39/// Entries are kept in the order they were read, and every entry is kept
40/// whether or not anything here understands it — a digital signature, a
41/// thumbnail, an embedded object, a part from a future version of the format.
42pub struct Package {
43    parts: Vec<Part>,
44    media_type: Option<String>,
45}
46
47/// The entry that carries the package's media type. It is first, stored rather
48/// than deflated, and the one entry whose position in the archive the
49/// specification fixes, so that a reader can identify the format from the first
50/// bytes of the file without inflating anything.
51const MIMETYPE: &str = "mimetype";
52
53impl Package {
54    /// Read a package from its bytes.
55    ///
56    /// # Errors
57    ///
58    /// The bytes are not a zip archive, or an entry cannot be decompressed.
59    pub fn read(bytes: &[u8]) -> Result<Self, Error> {
60        let mut archive = ZipArchive::new(Cursor::new(bytes))?;
61        let mut parts = Vec::with_capacity(archive.len());
62        for index in 0..archive.len() {
63            let mut entry = archive.by_index(index)?;
64            // A directory entry carries no bytes. It is kept rather than
65            // skipped because an office application writes several, and a
66            // package written back without them is not the package that was
67            // read.
68            let name = entry.name().to_owned();
69            let stored = entry.compression() == CompressionMethod::Stored;
70            if name.ends_with('/') {
71                parts.push(Part {
72                    name,
73                    data: Vec::new(),
74                    stored,
75                    directory: true,
76                });
77                continue;
78            }
79            let mut data = Vec::new();
80            entry
81                .read_to_end(&mut data)
82                .map_err(|e| Error::Package(e.to_string()))?;
83            parts.push(Part {
84                name,
85                data,
86                stored,
87                directory: false,
88            });
89        }
90        let mut package = Self {
91            parts,
92            media_type: None,
93        };
94        package.media_type = package.read_media_type();
95        Ok(package)
96    }
97
98    /// The media type the package declares.
99    ///
100    /// `None` for a package that declares none, which is legal: the
101    /// specification makes the `mimetype` entry optional, and a package written
102    /// without one is identified by its file extension alone.
103    pub fn media_type(&self) -> Option<&str> {
104        self.media_type.as_deref()
105    }
106
107    /// Read the declared media type: the `mimetype` entry, or failing that the
108    /// manifest's entry for the package as a whole, whose path is a single
109    /// slash. Called once, when the package is read.
110    fn read_media_type(&self) -> Option<String> {
111        if let Some(part) = self.part(MIMETYPE)
112            && let Ok(text) = std::str::from_utf8(&part.data)
113        {
114            let text = text.trim();
115            if !text.is_empty() {
116                return Some(text.to_owned());
117            }
118        }
119        let part = self.part("META-INF/manifest.xml")?;
120        let root = xml::parse(&part.data, "META-INF/manifest.xml").ok()?;
121        root.elements()
122            .find(|e| {
123                e.is(&Ns::Manifest, "file-entry") && e.attr(&Ns::Manifest, "full-path") == Some("/")
124            })
125            .and_then(|e| e.attr(&Ns::Manifest, "media-type"))
126            .filter(|t| !t.is_empty())
127            .map(ToOwned::to_owned)
128    }
129
130    /// An entry by name.
131    pub fn part(&self, name: &str) -> Option<&Part> {
132        self.parts.iter().find(|p| p.name == name)
133    }
134
135    /// Every entry, in the order they are stored.
136    pub fn parts(&self) -> impl Iterator<Item = &Part> {
137        self.parts.iter()
138    }
139
140    /// Parse an entry as XML.
141    ///
142    /// # Errors
143    ///
144    /// The entry is absent, or is not well-formed XML.
145    pub fn xml(&self, name: &'static str) -> Result<Element, Error> {
146        let part = self.part(name).ok_or(Error::MissingPart(name))?;
147        xml::parse(&part.data, name)
148    }
149
150    /// Parse an entry as XML, or give back an empty document if it is absent.
151    ///
152    /// `styles.xml`, `meta.xml` and `settings.xml` are each optional in a
153    /// package that has nothing to put in them, and a document with no metadata
154    /// is not a document that should fail to open.
155    ///
156    /// # Errors
157    ///
158    /// The entry is present and is not well-formed XML.
159    pub fn optional_xml(&self, name: &'static str) -> Result<Option<Element>, Error> {
160        match self.part(name) {
161            Some(part) => xml::parse(&part.data, name).map(Some),
162            None => Ok(None),
163        }
164    }
165
166    /// Replace an entry's bytes, adding it at the end if it was not there.
167    pub fn set_part(&mut self, name: &str, data: Vec<u8>) {
168        if let Some(part) = self.parts.iter_mut().find(|p| p.name == name) {
169            part.data = data;
170            return;
171        }
172        self.parts.push(Part {
173            name: name.to_owned(),
174            data,
175            stored: false,
176            directory: false,
177        });
178    }
179
180    /// Replace an entry with a serialized XML tree.
181    pub fn set_xml(&mut self, name: &str, root: &Element) {
182        self.set_part(name, xml::serialize(root));
183    }
184
185    /// Write the package back out.
186    ///
187    /// The `mimetype` entry goes first and uncompressed whatever order it was
188    /// read in, because that is the one ordering rule the format has and a
189    /// package that breaks it is identified by its extension alone.
190    ///
191    /// # Errors
192    ///
193    /// The underlying writer failed, which for a buffer in memory means the
194    /// allocation did.
195    pub fn write(&self) -> Result<Vec<u8>, Error> {
196        let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
197        let stored = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
198        let deflated = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
199
200        if let Some(part) = self.part(MIMETYPE) {
201            writer.start_file(MIMETYPE, stored)?;
202            writer
203                .write_all(&part.data)
204                .map_err(|e| Error::Package(e.to_string()))?;
205        }
206        for part in &self.parts {
207            if part.name == MIMETYPE {
208                continue;
209            }
210            if part.directory {
211                writer.add_directory(part.name.trim_end_matches('/'), stored)?;
212                continue;
213            }
214            let options = if part.stored { stored } else { deflated };
215            writer.start_file(part.name.as_str(), options)?;
216            writer
217                .write_all(&part.data)
218                .map_err(|e| Error::Package(e.to_string()))?;
219        }
220        Ok(writer.finish()?.into_inner())
221    }
222
223    /// Check the package declares one of the media types this reader opens.
224    ///
225    /// A format may have more than one: a Writer/Web document is a text document
226    /// and declares a media type of its own.
227    ///
228    /// # Errors
229    ///
230    /// The package declares a media type that is none of them. A package that
231    /// declares none is accepted: the caller asked for a format, and the file
232    /// extension is the only other thing that ever said.
233    pub fn expect_media_type(&self, wanted: &'static [&'static str]) -> Result<(), Error> {
234        match self.media_type() {
235            Some(found) if !wanted.contains(&found) => Err(Error::WrongFormat {
236                found: found.to_owned(),
237                wanted: wanted.first().copied().unwrap_or_default(),
238            }),
239            _ => Ok(()),
240        }
241    }
242}