1use std::collections::HashSet;
4use std::fs::File;
5use std::io::{self, Read, Seek, Write};
6use std::path::Path;
7
8use zip::write::SimpleFileOptions;
9use zip::{CompressionMethod, DateTime, ZipArchive, ZipWriter};
10
11use super::is_signed_entry;
12use super::{Content, ZipError, ZipLimit};
13use crate::digest::{DigestAlgorithm, EntryHasher};
14use crate::error::{Error, FileOperation};
15use crate::types::ArchivePath;
16
17#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
19pub enum Compression {
20 Stored,
21 Deflated,
22}
23
24#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
26pub struct EntryOptions {
27 pub compression: Compression,
28 pub alignment: u16,
31}
32
33impl EntryOptions {
34 pub const STORED: Self = Self { compression: Compression::Stored, alignment: 4 };
36 pub const DEFLATED: Self = Self { compression: Compression::Deflated, alignment: 1 };
38
39 pub const fn page_aligned(page_size: u16) -> Self {
42 Self { compression: Compression::Stored, alignment: page_size }
43 }
44
45 pub fn for_path(path: &ArchivePath) -> Self {
48 const PRECOMPRESSED: &[&str] = &[
49 "jpg", "jpeg", "png", "gif", "webp", "wav", "mp2", "mp3", "ogg", "aac", "mpg", "mpeg", "mid", "midi",
50 "smf", "jet", "rtttl", "imy", "xmf", "mp4", "m4a", "m4v", "3gp", "3gpp", "3g2", "3gpp2", "amr", "awb",
51 "wma", "wmv", "webm", "mkv", "zip", "jar", "apk", "gz", "zst", "br", "xz", "bz2", "7z", "ktx2", "basis",
52 ];
53 match path.extension() {
54 Some(ext) if PRECOMPRESSED.iter().any(|p| p.eq_ignore_ascii_case(ext)) => Self::STORED,
55 _ => Self::DEFLATED,
56 }
57 }
58
59 fn zip_options(self) -> SimpleFileOptions {
60 let (method, alignment) = match self.compression {
61 Compression::Stored => (CompressionMethod::Stored, self.alignment.max(1)),
62 Compression::Deflated => (CompressionMethod::Deflated, 1),
63 };
64 SimpleFileOptions::default()
65 .compression_method(method)
66 .last_modified_time(DateTime::default())
67 .unix_permissions(0o644)
68 .large_file(false)
69 .with_alignment(alignment)
70 }
71}
72
73pub(crate) struct ApkWriter<W: Write + Seek> {
80 zip: ZipWriter<W>,
81 names: HashSet<String>,
82 digest: Option<DigestAlgorithm>,
83 digests: Vec<(String, Vec<u8>)>,
84}
85
86impl<W: Write + Seek> ApkWriter<W> {
87 pub fn new(sink: W) -> Self {
88 Self { zip: ZipWriter::new(sink), names: HashSet::new(), digest: None, digests: Vec::new() }
89 }
90
91 pub fn digest_entries(mut self, algorithm: DigestAlgorithm) -> Self {
94 self.digest = Some(algorithm);
95 self
96 }
97
98 pub fn digests(&self) -> &[(String, Vec<u8>)] {
100 &self.digests
101 }
102
103 pub fn add(&mut self, path: &ArchivePath, data: &[u8], options: EntryOptions) -> Result<(), ZipError> {
105 self.add_reader(path, data, options).map(drop)
106 }
107
108 pub fn add_reader(
110 &mut self,
111 path: &ArchivePath,
112 reader: impl Read,
113 options: EntryOptions,
114 ) -> Result<u64, ZipError> {
115 self.start(path.as_str())?;
116 self.zip.start_file(path.as_str(), options.zip_options())?;
117 self.stream(path.as_str(), reader)
118 }
119
120 pub fn add_content(&mut self, path: &ArchivePath, content: &Content, options: EntryOptions) -> Result<u64, Error> {
123 match content {
124 Content::Bytes(bytes) => Ok(self.add_reader(path, &bytes[..], options)?),
125 Content::File(source) => self.add_file(path, source, options),
126 }
127 }
128
129 fn add_file(&mut self, path: &ArchivePath, source: &Path, options: EntryOptions) -> Result<u64, Error> {
130 let file = File::open(source).map_err(Error::file(FileOperation::Open, source))?;
131 let expected = file.metadata().map_err(Error::file(FileOperation::Inspect, source))?.len();
132 if expected >= u64::from(u32::MAX) {
133 return Err(ZipError::LimitExceeded { limit: ZipLimit::Size }.into());
134 }
135 let actual = self.add_reader(path, file.take(expected + 1), options)?;
137 if actual != expected {
138 return Err(ZipError::SourceChanged { name: path.to_string(), expected, actual }.into());
139 }
140 Ok(actual)
141 }
142
143 pub fn copy<R: Read + Seek>(
147 &mut self,
148 archive: &mut ZipArchive<R>,
149 index: usize,
150 alignment: u16,
151 ) -> Result<(), ZipError> {
152 let (name, encrypted, stored) = {
153 let entry = archive.by_index_data(index)?;
154 let name = String::from_utf8(entry.name_raw().to_vec()).map_err(|_| ZipError::NonUtf8Name { index })?;
155 (name, entry.encrypted(), entry.compression() == CompressionMethod::Stored)
156 };
157 if encrypted {
158 return Err(ZipError::EncryptedEntry { name });
159 }
160 let directory = name.ends_with('/');
164 ArchivePath::try_new(name.strip_suffix('/').unwrap_or(&name))
165 .map_err(|source| ZipError::UnsafeName { name: name.clone(), source })?;
166 self.start(&name)?;
167 if stored && !directory {
168 let options = EntryOptions { compression: Compression::Stored, alignment }.zip_options();
169 self.zip.start_file(name.as_str(), options)?;
170 let data = archive.by_index(index)?;
172 self.stream(&name, data)?;
173 return Ok(());
174 }
175 if let Some(algorithm) = self.digest.filter(|_| is_signed_entry(&name)) {
176 let mut hasher = algorithm.hasher();
177 io::copy(&mut archive.by_index(index)?, &mut hasher)?;
178 self.digests.push((name, hasher.finish()));
179 }
180 let raw = archive.by_index_raw(index)?;
181 self.zip.raw_copy_file_touch(raw, DateTime::default(), Some(0o644))?;
182 Ok(())
183 }
184
185 pub fn set_comment(&mut self, comment: &[u8]) -> Result<(), ZipError> {
187 Ok(self.zip.set_raw_comment(comment.into())?)
188 }
189
190 fn start(&mut self, name: &str) -> Result<(), ZipError> {
191 if self.names.len() >= usize::from(u16::MAX) - 1 {
192 return Err(ZipError::LimitExceeded { limit: ZipLimit::EntryCount });
193 }
194 if !self.names.insert(name.to_owned()) {
195 return Err(ZipError::DuplicateEntry { name: name.to_owned() });
196 }
197 Ok(())
198 }
199
200 fn stream(&mut self, name: &str, mut reader: impl Read) -> Result<u64, ZipError> {
202 let mut hasher = self.digest.filter(|_| is_signed_entry(name)).map(DigestAlgorithm::hasher);
203 let copied = io::copy(&mut reader, &mut Tee { zip: &mut self.zip, hasher: hasher.as_mut() });
204 let size = match copied {
205 Ok(size) => size,
206 Err(error) => {
207 let _ = self.zip.abort_file();
208 return Err(error.into());
209 }
210 };
211 if let Some(hasher) = hasher {
212 self.digests.push((name.to_owned(), hasher.finish()));
213 }
214 Ok(size)
215 }
216
217 pub fn finish(self) -> Result<W, ZipError> {
219 Ok(self.zip.finish()?)
220 }
221}
222
223struct Tee<'a, W: Write + Seek> {
225 zip: &'a mut ZipWriter<W>,
226 hasher: Option<&'a mut EntryHasher>,
227}
228
229impl<W: Write + Seek> Write for Tee<'_, W> {
230 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
231 let written = self.zip.write(buf)?;
232 if let Some(hasher) = self.hasher.as_deref_mut() {
233 hasher.update(&buf[..written]);
234 }
235 Ok(written)
236 }
237
238 fn flush(&mut self) -> io::Result<()> {
239 self.zip.flush()
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use crate::archive::{ApkLayout, open};
247 use std::io::Cursor;
248
249 fn path(p: &str) -> ArchivePath {
250 ArchivePath::try_new(p).unwrap()
251 }
252
253 fn data_offset(bytes: &[u8], name: &str) -> u64 {
254 let mut archive = ZipArchive::new(Cursor::new(bytes)).unwrap();
255 archive.by_name(name).unwrap().data_start().unwrap()
256 }
257
258 #[test]
259 fn aligns_stored_entries_and_is_reproducible() {
260 let build = || {
261 let mut writer = ApkWriter::new(Cursor::new(Vec::new()));
262 writer.add(&path("AndroidManifest.xml"), b"<manifest/>", EntryOptions::DEFLATED).unwrap();
263 writer.add(&path("resources.arsc"), b"table", EntryOptions::STORED).unwrap();
264 writer.add(&path("lib/arm64-v8a/libmain.so"), &[7; 1000], EntryOptions::page_aligned(16384)).unwrap();
265 writer.finish().unwrap().into_inner()
266 };
267 let bytes = build();
268 assert_eq!(bytes, build(), "identical inputs must produce identical archives");
269 assert_eq!(data_offset(&bytes, "resources.arsc") % 4, 0);
270 assert_eq!(data_offset(&bytes, "lib/arm64-v8a/libmain.so") % 16384, 0);
271 assert_eq!(ApkLayout::read(&mut Cursor::new(&bytes)).unwrap().entry_count(), 3);
272 }
273
274 #[test]
275 fn digests_signed_entries_and_rejects_duplicates() {
276 let mut writer = ApkWriter::new(Cursor::new(Vec::new())).digest_entries(DigestAlgorithm::Sha256);
277 writer.add(&path("a.txt"), b"abc", EntryOptions::DEFLATED).unwrap();
278 writer.add(&path("META-INF/CERT.SF"), b"not digested", EntryOptions::STORED).unwrap();
279 assert!(matches!(
280 writer.add(&path("a.txt"), b"again", EntryOptions::STORED),
281 Err(ZipError::DuplicateEntry { name }) if name == "a.txt"
282 ));
283 assert_eq!(writer.digests(), [("a.txt".to_owned(), DigestAlgorithm::Sha256.digest(b"abc"))]);
284 }
285
286 #[test]
287 fn copies_realign_stored_data_and_keep_compressed_data() {
288 let mut source = ZipWriter::new(Cursor::new(Vec::new()));
289 source
290 .start_file("lib/x86/libm.so", SimpleFileOptions::default().compression_method(CompressionMethod::Stored))
291 .unwrap();
292 source.write_all(&[1; 100]).unwrap();
293 let options =
295 SimpleFileOptions::default().compression_method(CompressionMethod::Deflated).compression_level(Some(1));
296 source.start_file("notes.txt", options).unwrap();
297 source.write_all(&[b'n'; 500]).unwrap();
298 let source = source.finish().unwrap().into_inner();
299 let (mut archive, _) = open(Cursor::new(&source)).unwrap();
300 let mut compressed = Vec::new();
301 archive.by_index_raw(1).unwrap().read_to_end(&mut compressed).unwrap();
302
303 let mut copy = ApkWriter::new(Cursor::new(Vec::new())).digest_entries(DigestAlgorithm::Sha256);
304 copy.copy(&mut archive, 0, 4096).unwrap();
305 copy.copy(&mut archive, 1, 4).unwrap();
306 let digests = copy.digests().to_vec();
307 let copied = copy.finish().unwrap().into_inner();
308
309 assert_eq!(data_offset(&copied, "lib/x86/libm.so") % 4096, 0);
310 let (mut copied_archive, _) = open(Cursor::new(&copied)).unwrap();
311 let mut copied_compressed = Vec::new();
312 copied_archive.by_index_raw(1).unwrap().read_to_end(&mut copied_compressed).unwrap();
313 assert_eq!(copied_compressed, compressed, "deflated bytes changed");
314 let mut text = Vec::new();
315 copied_archive.by_name("notes.txt").unwrap().read_to_end(&mut text).unwrap();
316 assert_eq!(text, [b'n'; 500]);
317 assert_eq!(digests[1], ("notes.txt".to_owned(), DigestAlgorithm::Sha256.digest(&[b'n'; 500])));
318 }
319}