1use std::io::{Cursor, Read, Seek, Write};
4
5use zip::write::SimpleFileOptions;
6use zip::{CompressionMethod, ZipArchive, ZipWriter};
7
8use crate::{manifest, LibError, Result, MIMETYPE};
9
10#[derive(Debug, Clone)]
12pub struct DataFile {
13 pub name: String,
15 pub mime_type: String,
17 pub content: Vec<u8>,
19}
20
21#[derive(Debug, Clone)]
23pub struct SignatureFile {
24 pub name: String,
26 pub xml: String,
28}
29
30#[derive(Debug, Clone)]
32pub struct OpenOptions {
33 pub require_mimetype: bool,
38}
39
40impl Default for OpenOptions {
41 fn default() -> Self {
42 Self {
43 require_mimetype: false,
44 }
45 }
46}
47
48#[derive(Debug, Default)]
50pub struct Container {
51 data_files: Vec<DataFile>,
52 signatures: Vec<SignatureFile>,
53 warnings: Vec<String>,
55}
56
57impl Container {
58 pub fn new() -> Self {
60 Self::default()
61 }
62
63 pub fn data_files(&self) -> &[DataFile] {
65 &self.data_files
66 }
67
68 pub fn signatures(&self) -> &[SignatureFile] {
70 &self.signatures
71 }
72
73 pub fn warnings(&self) -> &[String] {
75 &self.warnings
76 }
77
78 pub fn add_file(
83 &mut self,
84 name: impl Into<String>,
85 mime_type: impl Into<String>,
86 content: Vec<u8>,
87 ) -> Result<()> {
88 let name = name.into();
89 validate_entry_name(&name)?;
90 if self.data_files.iter().any(|f| f.name == name) {
91 return Err(LibError::Container(format!("duplicate file name: {name}")));
92 }
93 self.data_files.push(DataFile {
94 name,
95 mime_type: mime_type.into(),
96 content,
97 });
98 Ok(())
99 }
100
101 pub fn add_signature_xml(&mut self, xml: String) -> &SignatureFile {
106 let mut n = self.signatures.len();
107 let name = loop {
108 let candidate = format!("META-INF/signatures{n}.xml");
109 if !self.signatures.iter().any(|s| s.name == candidate) {
110 break candidate;
111 }
112 n += 1;
113 };
114 self.signatures.push(SignatureFile { name, xml });
115 self.signatures.last().expect("a signature should exist")
116 }
117
118 pub fn open<R: Read + Seek>(reader: R) -> Result<Self> {
120 Self::open_with(reader, &OpenOptions::default())
121 }
122
123 pub fn open_with<R: Read + Seek>(reader: R, opts: &OpenOptions) -> Result<Self> {
125 let mut zip = ZipArchive::new(reader)?;
126 let mut container = Container::default();
127
128 let mut names: Vec<String> = Vec::with_capacity(zip.len());
129 for i in 0..zip.len() {
130 names.push(zip.by_index(i)?.name().to_owned());
131 }
132
133 let mimetype_present = names.iter().any(|n| n == "mimetype");
138 if let Some(n) = names.first() {
139 if n == "mimetype" {
140 let entry = zip.by_index(0)?;
141 if entry.compression() != CompressionMethod::Stored {
142 container.warnings.push("'mimetype' is compressed".into());
143 }
144 } else if mimetype_present {
145 container
146 .warnings
147 .push("'mimetype' is not the first zip entry".into());
148 }
149 }
150 if mimetype_present {
151 let mimetype = read_entry(&mut zip, "mimetype")?;
152 let mimetype = String::from_utf8_lossy(&mimetype);
153 if mimetype.trim() != MIMETYPE {
154 return Err(LibError::Container(format!(
155 "unexpected container mime type: {}",
156 mimetype.trim()
157 )));
158 }
159 } else if opts.require_mimetype {
160 return Err(LibError::Container("missing mimetype entry".into()));
161 } else {
162 container
163 .warnings
164 .push("container has no mimetype entry".into());
165 }
166
167 let manifest_types = match read_entry(&mut zip, "META-INF/manifest.xml") {
170 Ok(bytes) => manifest::parse(&bytes)?,
171 Err(_) => {
172 return Err(LibError::Container(
173 "container has no META-INF/manifest.xml".into(),
174 ));
175 }
176 };
177
178 for name in &names {
179 if name == "mimetype" || name.ends_with('/') {
180 continue;
181 }
182 let bytes = read_entry(&mut zip, name)?;
183 if let Some(rest) = name.strip_prefix("META-INF/") {
184 if is_signature_entry(rest) {
185 container.signatures.push(SignatureFile {
186 name: name.clone(),
187 xml: String::from_utf8(bytes)
188 .map_err(|_| LibError::Xml(format!("{name} is not valid UTF-8")))?,
189 });
190 }
191 continue;
197 }
198 let mime_type = manifest_types
199 .iter()
200 .find(|(n, _)| n == name)
201 .map(|(_, m)| m.clone())
202 .ok_or_else(|| {
203 LibError::Container(format!("missing MIME type in manifest for {name}"))
204 })?;
205 container.data_files.push(DataFile {
206 name: name.clone(),
207 mime_type,
208 content: bytes,
209 });
210 }
211
212 if container.data_files.is_empty() {
213 return Err(LibError::Container("container has no data files".into()));
214 }
215 Ok(container)
216 }
217
218 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
220 Self::open(Cursor::new(bytes))
221 }
222
223 pub fn from_bytes_with(bytes: &[u8], opts: &OpenOptions) -> Result<Self> {
225 Self::open_with(Cursor::new(bytes), opts)
226 }
227
228 pub fn open_file(path: impl AsRef<std::path::Path>) -> Result<Self> {
230 Self::open(std::fs::File::open(path)?)
231 }
232
233 pub fn open_file_with(path: impl AsRef<std::path::Path>, opts: &OpenOptions) -> Result<Self> {
235 Self::open_with(std::fs::File::open(path)?, opts)
236 }
237
238 fn write_to<W: Write + Seek>(&self, writer: W) -> Result<()> {
239 if self.data_files.is_empty() {
240 return Err(LibError::Container("container has no data files".into()));
241 }
242 let mut zip = ZipWriter::new(writer);
243
244 zip.start_file(
245 "mimetype",
246 SimpleFileOptions::default().compression_method(CompressionMethod::Stored),
247 )?;
248 zip.write_all(MIMETYPE.as_bytes())?;
249
250 let deflated = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
251 for file in &self.data_files {
252 zip.start_file(&file.name, deflated)?;
253 zip.write_all(&file.content)?;
254 }
255
256 zip.start_file("META-INF/manifest.xml", deflated)?;
257 zip.write_all(&manifest::build(&self.data_files))?;
258
259 for sig in &self.signatures {
260 zip.start_file(&sig.name, deflated)?;
261 zip.write_all(sig.xml.as_bytes())?;
262 }
263
264 zip.finish()?;
265 Ok(())
266 }
267
268 pub fn to_bytes(&self) -> Result<Vec<u8>> {
270 let mut buf = Cursor::new(Vec::new());
271 self.write_to(&mut buf)?;
272 Ok(buf.into_inner())
273 }
274
275 pub fn save(&self, path: impl AsRef<std::path::Path>) -> Result<()> {
277 self.write_to(std::fs::File::create(path)?)
278 }
279}
280
281fn read_entry<R: Read + Seek>(zip: &mut ZipArchive<R>, name: &str) -> Result<Vec<u8>> {
282 let mut entry = zip
283 .by_name(name)
284 .map_err(|_| LibError::Container(format!("missing zip entry: {name}")))?;
285 let mut buf = Vec::with_capacity(entry.size() as usize);
286 entry.read_to_end(&mut buf)?;
287 Ok(buf)
288}
289
290fn validate_entry_name(name: &str) -> Result<()> {
291 if name.is_empty() {
292 return Err(LibError::Container("file name must not be empty".into()));
293 }
294 if name == "mimetype" || name.starts_with("META-INF/") {
295 return Err(LibError::Container(format!("reserved entry name: {name}")));
296 }
297 if name.starts_with('/') || name.split('/').any(|part| part == ".." || part.is_empty()) {
298 return Err(LibError::Container(format!(
299 "file name must be a clean relative path: {name}"
300 )));
301 }
302 Ok(())
303}
304
305fn is_signature_entry(meta_inf_rest: &str) -> bool {
306 meta_inf_rest.contains("signatures") && meta_inf_rest.ends_with(".xml")
308}