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}