sevenz_rust2/util/
compress.rs

1//! 7z Compressor helper functions
2
3use std::{
4    fs::File,
5    io::{Seek, Write},
6    path::{Path, PathBuf},
7};
8
9#[cfg(feature = "aes256")]
10use crate::encoder_options::AesEncoderOptions;
11use crate::{ArchiveEntry, ArchiveWriter, EncoderMethod, Error, Password, writer::LazyFileReader};
12
13/// Compresses a source file or directory to a destination writer.
14///
15/// # Arguments
16/// * `src` - Path to the source file or directory to compress
17/// * `dest` - Writer that implements `Write + Seek` to write the compressed archive to
18pub fn compress<W: Write + Seek>(src: impl AsRef<Path>, dest: W) -> Result<W, Error> {
19    let mut archive_writer = ArchiveWriter::new(dest)?;
20    let parent = if src.as_ref().is_dir() {
21        src.as_ref()
22    } else {
23        src.as_ref().parent().unwrap_or(src.as_ref())
24    };
25    compress_path(src.as_ref(), parent, &mut archive_writer)?;
26    Ok(archive_writer.finish()?)
27}
28
29/// Compresses a source file or directory to a destination writer with password encryption.
30///
31/// # Arguments
32/// * `src` - Path to the source file or directory to compress
33/// * `dest` - Writer that implements `Write + Seek` to write the compressed archive to
34/// * `password` - Password to encrypt the archive with
35#[cfg(feature = "aes256")]
36pub fn compress_encrypted<W: Write + Seek>(
37    src: impl AsRef<Path>,
38    dest: W,
39    password: Password,
40) -> Result<W, Error> {
41    let mut archive_writer = ArchiveWriter::new(dest)?;
42    if !password.is_empty() {
43        archive_writer.set_content_methods(vec![
44            AesEncoderOptions::new(password).into(),
45            EncoderMethod::LZMA2.into(),
46        ]);
47    }
48    let parent = if src.as_ref().is_dir() {
49        src.as_ref()
50    } else {
51        src.as_ref().parent().unwrap_or(src.as_ref())
52    };
53    compress_path(src.as_ref(), parent, &mut archive_writer)?;
54    Ok(archive_writer.finish()?)
55}
56
57/// Compresses a source file or directory to a destination file path.
58///
59/// This is a convenience function that handles file creation automatically.
60///
61/// # Arguments
62/// * `src` - Path to the source file or directory to compress
63/// * `dest` - Path where the compressed archive will be created
64pub fn compress_to_path(src: impl AsRef<Path>, dest: impl AsRef<Path>) -> Result<(), Error> {
65    if let Some(path) = dest.as_ref().parent() {
66        if !path.exists() {
67            std::fs::create_dir_all(path)
68                .map_err(|e| Error::io_msg(e, format!("Create dir failed:{:?}", dest.as_ref())))?;
69        }
70    }
71    compress(
72        src,
73        File::create(dest.as_ref())
74            .map_err(|e| Error::file_open(e, dest.as_ref().to_string_lossy().to_string()))?,
75    )?;
76    Ok(())
77}
78
79/// Compresses a source file or directory to a destination file path with password encryption.
80///
81/// This is a convenience function that handles file creation automatically.
82///
83/// # Arguments
84/// * `src` - Path to the source file or directory to compress
85/// * `dest` - Path where the encrypted compressed archive will be created
86/// * `password` - Password to encrypt the archive with
87#[cfg(feature = "aes256")]
88pub fn compress_to_path_encrypted(
89    src: impl AsRef<Path>,
90    dest: impl AsRef<Path>,
91    password: Password,
92) -> Result<(), Error> {
93    if let Some(path) = dest.as_ref().parent() {
94        if !path.exists() {
95            std::fs::create_dir_all(path)
96                .map_err(|e| Error::io_msg(e, format!("Create dir failed:{:?}", dest.as_ref())))?;
97        }
98    }
99    compress_encrypted(
100        src,
101        File::create(dest.as_ref())
102            .map_err(|e| Error::file_open(e, dest.as_ref().to_string_lossy().to_string()))?,
103        password,
104    )?;
105    Ok(())
106}
107
108fn compress_path<W: Write + Seek, P: AsRef<Path>>(
109    src: P,
110    root: &Path,
111    archive_writer: &mut ArchiveWriter<W>,
112) -> Result<(), Error> {
113    let entry_name = src
114        .as_ref()
115        .strip_prefix(root)
116        .map_err(|e| Error::other(e.to_string()))?
117        .to_string_lossy()
118        .to_string();
119    let entry = ArchiveEntry::from_path(src.as_ref(), entry_name);
120    let path = src.as_ref();
121    if path.is_dir() {
122        archive_writer.push_archive_entry::<&[u8]>(entry, None)?;
123        for dir in path
124            .read_dir()
125            .map_err(|e| Error::io_msg(e, "error read dir"))?
126        {
127            let dir = dir?;
128            let ftype = dir.file_type()?;
129            if ftype.is_dir() || ftype.is_file() {
130                compress_path(dir.path(), root, archive_writer)?;
131            }
132        }
133    } else {
134        archive_writer.push_archive_entry(
135            entry,
136            Some(
137                File::open(path)
138                    .map_err(|e| Error::file_open(e, path.to_string_lossy().to_string()))?,
139            ),
140        )?;
141    }
142    Ok(())
143}
144
145impl<W: Write + Seek> ArchiveWriter<W> {
146    /// Adds a source path to the compression builder with a filter function using solid compression.
147    ///
148    /// The filter function allows selective inclusion of files based on their paths.
149    /// Files are compressed using solid compression for better compression ratios.
150    ///
151    /// # Arguments
152    /// * `path` - Path to add to the compression
153    /// * `filter` - Function that returns `true` for paths that should be included
154    pub fn push_source_path(
155        &mut self,
156        path: impl AsRef<Path>,
157        filter: impl Fn(&Path) -> bool,
158    ) -> Result<&mut Self, Error> {
159        encode_path(true, &path, self, filter)?;
160        Ok(self)
161    }
162
163    /// Adds a source path to the compression builder with a filter function using non-solid compression.
164    ///
165    /// Non-solid compression allows individual file extraction without decompressing the entire archive,
166    /// but typically results in larger archive sizes compared to solid compression.
167    ///
168    /// # Arguments
169    /// * `path` - Path to add to the compression
170    /// * `filter` - Function that returns `true` for paths that should be included
171    pub fn push_source_path_non_solid(
172        &mut self,
173        path: impl AsRef<Path>,
174        filter: impl Fn(&Path) -> bool,
175    ) -> Result<&mut Self, Error> {
176        encode_path(false, &path, self, filter)?;
177        Ok(self)
178    }
179}
180
181fn collect_file_paths(
182    src: impl AsRef<Path>,
183    paths: &mut Vec<PathBuf>,
184    filter: &dyn Fn(&Path) -> bool,
185) -> std::io::Result<()> {
186    let path = src.as_ref();
187    if !filter(path) {
188        return Ok(());
189    }
190    if path.is_dir() {
191        for dir in path.read_dir()? {
192            let dir = dir?;
193            let ftype = dir.file_type()?;
194            if ftype.is_file() || ftype.is_dir() {
195                collect_file_paths(dir.path(), paths, filter)?;
196            }
197        }
198    } else {
199        paths.push(path.to_path_buf())
200    }
201    Ok(())
202}
203
204const MAX_BLOCK_SIZE: u64 = 4 * 1024 * 1024 * 1024; // 4 GiB
205
206fn encode_path<W: Write + Seek>(
207    solid: bool,
208    src: impl AsRef<Path>,
209    zip: &mut ArchiveWriter<W>,
210    filter: impl Fn(&Path) -> bool,
211) -> Result<(), Error> {
212    let mut entries = Vec::new();
213    let mut paths = Vec::new();
214    collect_file_paths(&src, &mut paths, &filter).map_err(|e| {
215        Error::io_msg(
216            e,
217            format!("Failed to collect entries from path:{:?}", src.as_ref()),
218        )
219    })?;
220
221    if !solid {
222        for ele in paths.into_iter() {
223            let name = extract_file_name(&src, &ele)?;
224
225            zip.push_archive_entry(
226                ArchiveEntry::from_path(ele.as_path(), name),
227                Some(File::open(ele.as_path())?),
228            )?;
229        }
230        return Ok(());
231    }
232    let mut files = Vec::new();
233    let mut file_size = 0;
234    for ele in paths.into_iter() {
235        let size = ele.metadata()?.len();
236        let name = extract_file_name(&src, &ele)?;
237
238        if size >= MAX_BLOCK_SIZE {
239            zip.push_archive_entry(
240                ArchiveEntry::from_path(ele.as_path(), name),
241                Some(File::open(ele.as_path())?),
242            )?;
243            continue;
244        }
245        if file_size + size >= MAX_BLOCK_SIZE {
246            zip.push_archive_entries(entries, files)?;
247            entries = Vec::new();
248            files = Vec::new();
249            file_size = 0;
250        }
251        file_size += size;
252        entries.push(ArchiveEntry::from_path(ele.as_path(), name));
253        files.push(LazyFileReader::new(ele).into());
254    }
255    if !entries.is_empty() {
256        zip.push_archive_entries(entries, files)?;
257    }
258
259    Ok(())
260}
261
262fn extract_file_name(src: &impl AsRef<Path>, ele: &PathBuf) -> Result<String, Error> {
263    if ele == src.as_ref() {
264        // Single file case: use just the filename.
265        Ok(ele
266            .file_name()
267            .ok_or_else(|| {
268                Error::io_msg(
269                    std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid filename"),
270                    format!("Failed to get filename from {ele:?}"),
271                )
272            })?
273            .to_string_lossy()
274            .to_string())
275    } else {
276        // Directory case: remove path.
277        Ok(ele.strip_prefix(src).unwrap().to_string_lossy().to_string())
278    }
279}