Skip to main content

loadsmith_install/ops/
extract.rs

1use std::{
2    fs::File,
3    io::{self, Read, Seek},
4    path::{Path, PathBuf},
5};
6
7use tracing::{trace, warn};
8
9use crate::{
10    error::Result,
11    zip::{Zip, ZipFile},
12};
13
14/// Extract all files from a zip archive into a target directory.
15///
16/// Directories inside the archive are skipped; parent directories are created as
17/// needed. Files that already exist at the target path are skipped with a warning.
18///
19/// On Unix, file permissions from the zip entry are applied after extraction.
20///
21/// # Examples
22///
23/// ```rust,no_run
24/// use std::fs::File;
25/// use loadsmith_install::extract;
26///
27/// let file = File::open("C:\\mods\\package.zip").unwrap();
28/// let files = extract(file, "C:\\output").unwrap();
29/// println!("extracted {} files", files.len());
30/// ```
31pub fn extract<R: Read + Seek>(reader: R, target: impl AsRef<Path>) -> Result<Vec<PathBuf>> {
32    let mut zip = zip::ZipArchive::new(reader)?;
33    extract_zip(&mut zip, target.as_ref())
34}
35
36fn extract_zip<Z: Zip>(zip: &mut Z, target: impl AsRef<Path>) -> Result<Vec<PathBuf>> {
37    let t = crate::zip::private::Token;
38
39    let target = target.as_ref();
40    let mut files = Vec::new();
41
42    for i in 0..zip.len(t) {
43        let mut source_file = zip.by_index(i, t)?;
44
45        if source_file.is_dir(t) {
46            continue; // we create the necessary dirs when creating files instead
47        }
48
49        let relative_path = source_file.path(t)?;
50
51        let target_path = target.join(&relative_path);
52
53        if target_path.exists() {
54            warn!(%relative_path, "file already exists, skipping extraction");
55            continue;
56        }
57
58        trace!(%relative_path, "extract file");
59
60        loadsmith_util::create_parent_dirs(&target_path)?;
61
62        let mut target_file = File::create(&target_path)?;
63
64        io::copy(&mut source_file, &mut target_file)?;
65
66        #[cfg(unix)]
67        set_unix_mode(&source_file, &target_path)?;
68
69        files.push(target_path);
70    }
71
72    Ok(files)
73}
74
75#[cfg(unix)]
76fn set_unix_mode<F: ZipFile>(file: &F, path: &Path) -> Result<()> {
77    use std::os::unix::fs::PermissionsExt;
78
79    if let Some(mode) = file.unix_mode(crate::zip::private::Token) {
80        std::fs::set_permissions(path, PermissionsExt::from_mode(mode))?;
81    }
82
83    Ok(())
84}
85
86#[cfg(test)]
87mod tests {
88    use crate::zip::mock::MockZip;
89
90    use super::*;
91
92    #[test]
93    fn simple_glob() {
94        let included_files = &["file1", "nested/file2", "nested/../file3", "./././file4"];
95
96        let mut zip = MockZip::default();
97
98        for file in included_files {
99            zip = zip.with_empty_file(file);
100        }
101
102        let dir = tempfile::tempdir().unwrap();
103
104        extract_zip(&mut zip, &dir).unwrap();
105
106        for file in included_files {
107            assert!(dir.path().join(file).exists());
108        }
109    }
110}