volaris_tools/
unpack.rs

1//! This contains the logic for decrypting a zip file, and extracting each file to the target directory. The temporary zip file is then erased with one pass.
2//!
3//! This is known as "unpacking" within volaris.
4
5use std::cell::RefCell;
6use std::io::{Read, Seek, Write};
7use std::path::PathBuf;
8use std::sync::Arc;
9
10use crate::storage::{self, Storage};
11use crate::{decrypt, overwrite};
12use corecrypto::protected::Protected;
13
14#[derive(Debug)]
15pub enum Error {
16    WriteData,
17    OpenArchive,
18    OpenArchivedFile,
19    ResetCursorPosition,
20    Storage(storage::Error),
21    Decrypt(decrypt::Error),
22}
23
24impl std::fmt::Display for Error {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            Error::WriteData => f.write_str("Unable to write data"),
28            Error::OpenArchive => f.write_str("Unable to open archive"),
29            Error::OpenArchivedFile => f.write_str("Unable to open archived file"),
30            Error::ResetCursorPosition => f.write_str("Unable to reset cursor position"),
31            Error::Storage(inner) => write!(f, "Storage error: {inner}"),
32            Error::Decrypt(inner) => write!(f, "Decrypt error: {inner}"),
33        }
34    }
35}
36
37impl std::error::Error for Error {}
38
39type OnArchiveInfo = Box<dyn FnOnce(usize)>;
40type OnZipFileFn = Box<dyn Fn(PathBuf) -> bool>;
41
42pub struct Request<'a, R>
43where
44    R: Read,
45{
46    pub reader: &'a RefCell<R>,
47    pub header_reader: Option<&'a RefCell<R>>,
48    pub raw_key: Protected<Vec<u8>>,
49    pub output_dir_path: PathBuf,
50    pub on_decrypted_header: Option<decrypt::OnDecryptedHeaderFn>,
51    pub on_archive_info: Option<OnArchiveInfo>,
52    pub on_zip_file: Option<OnZipFileFn>,
53}
54
55pub fn execute<RW: Read + Write + Seek>(
56    stor: Arc<impl Storage<RW> + 'static>,
57    req: Request<'_, RW>,
58) -> Result<(), Error> {
59    // 1. Create temp zip archive.
60    let tmp_file = stor.create_temp_file().map_err(Error::Storage)?;
61
62    // 2. Decrypt input file to temp zip archive.
63    decrypt::execute(decrypt::Request {
64        header_reader: req.header_reader,
65        reader: req.reader,
66        writer: tmp_file
67            .try_writer()
68            .expect("We sure that file in write mode"),
69        raw_key: req.raw_key,
70        on_decrypted_header: req.on_decrypted_header,
71    })
72    .map_err(Error::Decrypt)?;
73
74    let buf_capacity = stor.file_len(&tmp_file).map_err(Error::Storage)?;
75
76    // 3. Recover files from temp archive.
77    {
78        let mut reader = tmp_file
79            .try_reader()
80            .expect("We sure that file in read mode")
81            .borrow_mut();
82
83        reader.rewind().map_err(|_| Error::ResetCursorPosition)?;
84
85        let mut archive = zip::ZipArchive::new(&mut *reader).map_err(|_| Error::OpenArchive)?;
86
87        let output_dir = req.output_dir_path.clone();
88
89        // 4. prepare phase
90        let entities = (0..archive.len())
91            .filter_map(|i| {
92                let zip_file = archive.by_index(i).ok()?;
93                let mut full_path = output_dir.clone();
94
95                // Prevent zip slip attack
96                //
97                // Source: https://snyk.io/research/zip-slip-vulnerability
98                zip_file.enclosed_name().map(|path| {
99                    full_path.push(path);
100
101                    (full_path, i, zip_file.is_dir())
102                })
103            })
104            .filter(|(full_path, ..)| {
105                if let Some(on_zip_file) = req.on_zip_file.as_ref() {
106                    on_zip_file(full_path.clone())
107                } else {
108                    true
109                }
110            })
111            .collect::<Vec<_>>();
112
113        let files_count = entities.len();
114        if let Some(on_archive_info) = req.on_archive_info {
115            on_archive_info(files_count);
116        }
117
118        // 5. create dirs
119        #[allow(clippy::needless_collect)]
120        let create_dirs_jobs = entities
121            .iter()
122            .filter(|(_, _, is_dir)| *is_dir)
123            .map(|(fp, ..)| fp)
124            .chain([&output_dir])
125            .map(|full_path| {
126                let stor = stor.clone();
127                let full_path = full_path.clone();
128                std::thread::spawn(move || stor.create_dir_all(full_path).map_err(Error::Storage))
129            })
130            .collect::<Vec<_>>();
131
132        create_dirs_jobs
133            .into_iter()
134            .try_for_each(|th| th.join().unwrap())?;
135
136        // 6. create files
137        entities
138            .iter()
139            .filter(|(_, _, is_dir)| !*is_dir)
140            .try_for_each(|(full_path, i, _)| {
141                let mut zip_file = archive.by_index(*i).map_err(|_| Error::OpenArchivedFile)?;
142                let file = stor
143                    .create_file(full_path)
144                    .or_else(|_| stor.write_file(full_path))
145                    .map_err(Error::Storage)?;
146                std::io::copy(
147                    &mut zip_file,
148                    &mut *file.try_writer().map_err(Error::Storage)?.borrow_mut(),
149                )
150                .map_err(|_| Error::WriteData)?;
151                Ok(())
152            })?;
153    }
154
155    // 7. Finally eraze temp zip archive with zeros.
156    overwrite::execute(overwrite::Request {
157        buf_capacity,
158        writer: tmp_file
159            .try_writer()
160            .expect("We sure that file in write mode"),
161        passes: 1,
162    })
163    .ok();
164
165    stor.remove_file(tmp_file).ok();
166
167    Ok(())
168}
169
170#[cfg(test)]
171mod tests {
172    #[test]
173    #[ignore = "not yet implemented"]
174    fn should_unpack_encrypted_archive() {
175        todo!()
176    }
177}