Skip to main content

simple_archive/
writer.rs

1use crate::prelude::*;
2
3use std::{
4    ffi::CString,
5    fs::File,
6    io::{Read, Write},
7    ptr::null_mut,
8};
9
10use libc::c_void;
11
12use crate::{
13    carchive::{
14        self, archive, archive_entry_free, archive_entry_new, archive_entry_set_atime,
15        archive_entry_set_ctime, archive_entry_set_gid, archive_entry_set_mode,
16        archive_entry_set_mtime, archive_entry_set_pathname, archive_entry_set_perm,
17        archive_entry_set_size, archive_entry_set_uid, archive_write_data, archive_write_free,
18        archive_write_header,
19    },
20    ARCHIVE_FILTER_BZIP2, ARCHIVE_FILTER_GZIP, ARCHIVE_FILTER_LRZIP, ARCHIVE_FILTER_LZ4,
21    ARCHIVE_FILTER_LZIP, ARCHIVE_FILTER_LZMA, ARCHIVE_FILTER_LZOP, ARCHIVE_FILTER_NONE,
22    ARCHIVE_FILTER_XZ, ARCHIVE_FILTER_ZSTD, ARCHIVE_FORMAT_7ZIP, ARCHIVE_FORMAT_TAR,
23    ARCHIVE_FORMAT_XAR, ARCHIVE_FORMAT_ZIP,
24};
25use std::os::raw::c_int;
26
27const BUFFER_SIZE: usize = 16384;
28
29/// A writer for creating compressed archives.
30///
31/// `ArchiveWriter` allows creating archives in any destination that implements `Write`.
32/// It supports various formats (TAR, ZIP, 7ZIP, etc.) and compression filters (GZIP, BZIP2, XZ, etc.).
33pub struct ArchiveWriter<W: Write> {
34    archive_writer: *mut archive,
35    fileref: Box<FileWriter<W>>,
36    file_format: c_int,
37    file_filter: c_int,
38}
39
40struct FileWriter<W: Write> {
41    obj: W,
42}
43
44unsafe extern "C" fn archivewriter_write<W: Write>(
45    archive: *mut carchive::archive,
46    client_data: *mut c_void,
47    buffer: *const c_void,
48    size: usize,
49) -> carchive::la_ssize_t {
50    let writer = (client_data as *mut FileWriter<W>).as_mut().unwrap();
51    let writable = std::slice::from_raw_parts(buffer as *const u8, size);
52
53    match writer.obj.write(writable) {
54        Ok(size) => size as carchive::la_ssize_t,
55        Err(e) => {
56            let description = CString::new(e.to_string()).unwrap();
57            carchive::archive_set_error(
58                archive,
59                e.raw_os_error().unwrap_or(0),
60                description.as_ptr(),
61            );
62            -1
63        }
64    }
65}
66
67impl<W: Write> ArchiveWriter<W> {
68    /// Creates a new `ArchiveWriter` that writes to the given destination.
69    pub fn new(dest: W) -> Result<ArchiveWriter<W>>
70    where
71        W: Write,
72    {
73        let fref = Box::new(FileWriter { obj: dest });
74        unsafe {
75            let archive_writer = carchive::archive_write_new();
76
77            if archive_writer.is_null() {
78                return Err(Error::NullArchive);
79            }
80
81            match carchive::archive_write_set_bytes_in_last_block(archive_writer, 1) {
82                carchive::ARCHIVE_OK | carchive::ARCHIVE_WARN => (),
83                _ => return Err(Error::from(archive_writer)),
84            };
85
86            Ok(ArchiveWriter {
87                archive_writer,
88                fileref: fref,
89                file_format: -1,
90                file_filter: -1,
91            })
92        }
93    }
94
95    /// Sets the output format for the archive (e.g., TAR, ZIP).
96    pub fn set_output_format(&mut self, format: c_int) -> Result<()> {
97        match unsafe { carchive::archive_write_set_format(self.archive_writer, format) } {
98            carchive::ARCHIVE_OK | carchive::ARCHIVE_WARN => (),
99            _ => return Err(Error::from(self.archive_writer)),
100        }
101
102        self.file_format = format;
103        Ok(())
104    }
105
106    /// Sets the output filter (compression) for the archive (e.g., GZIP, BZIP2).
107    pub fn set_output_filter(&mut self, filter: c_int) -> Result<()> {
108        match unsafe { carchive::archive_write_add_filter(self.archive_writer, filter) } {
109            carchive::ARCHIVE_OK | carchive::ARCHIVE_WARN => (),
110            _ => return Err(Error::from(self.archive_writer)),
111        }
112        self.file_filter = filter;
113        Ok(())
114    }
115
116    pub fn add_filter_option(&mut self, name: &str, value: &str) -> Result<()> {
117        let n = CString::new(name.to_string()).unwrap();
118        let v = CString::new(value.to_string()).unwrap();
119        match unsafe {
120            carchive::archive_write_set_filter_option(
121                self.archive_writer,
122                null_mut(),
123                n.as_ptr(),
124                v.as_ptr(),
125            )
126        } {
127            carchive::ARCHIVE_OK | carchive::ARCHIVE_WARN => (),
128            _ => return Err(Error::from(self.archive_writer)),
129        }
130        Ok(())
131    }
132
133    pub fn add_format_option(&mut self, name: &str, value: &str) -> Result<()> {
134        let n = CString::new(name.to_string()).unwrap();
135        let v = CString::new(value.to_string()).unwrap();
136        match unsafe {
137            carchive::archive_write_set_format_option(
138                self.archive_writer,
139                null_mut(),
140                n.as_ptr(),
141                v.as_ptr(),
142            )
143        } {
144            carchive::ARCHIVE_OK | carchive::ARCHIVE_WARN => (),
145            _ => return Err(Error::from(self.archive_writer)),
146        }
147        Ok(())
148    }
149
150    /// Opens the archive for writing.
151    /// This must be called after setting the format and filter, but before adding files.
152    pub fn open(&mut self) -> Result<()> {
153        if self.file_format < 0 || self.file_filter < 0 {
154            return Err(Error::IncompleteInitialization);
155        }
156        match unsafe {
157            carchive::archive_write_open(
158                self.archive_writer,
159                std::ptr::addr_of_mut!(*self.fileref) as *mut c_void,
160                None,
161                Some(archivewriter_write::<W>),
162                None,
163            )
164        } {
165            carchive::ARCHIVE_OK | carchive::ARCHIVE_WARN => (),
166            _ => return Err(Error::from(self.archive_writer)),
167        }
168        Ok(())
169    }
170
171    // this free is not meant to called directly. Only by borrow system
172    fn free(&mut self) -> Result<()> {
173        match unsafe { archive_write_free(self.archive_writer) } {
174            carchive::ARCHIVE_OK | carchive::ARCHIVE_WARN => Ok(()),
175            _ => Err(Error::from(self.archive_writer)),
176        }
177    }
178
179    // this is only for output write filter.
180    pub fn set_compression_high(&mut self) -> Result<()> {
181        let mut max_compression_level = match self.file_filter {
182            ARCHIVE_FILTER_BZIP2 => 9,
183            ARCHIVE_FILTER_GZIP => 9,
184            ARCHIVE_FILTER_LRZIP => 9,
185            ARCHIVE_FILTER_XZ => 9,
186            ARCHIVE_FILTER_LZ4 => 9,
187            ARCHIVE_FILTER_LZIP => 9,
188            ARCHIVE_FILTER_ZSTD => 22,
189            ARCHIVE_FILTER_LZMA => 9,
190            ARCHIVE_FILTER_LZOP => 9,
191            _ => -1,
192        };
193
194        if max_compression_level > 0 {
195            self.add_filter_option("compression-level", &max_compression_level.to_string())?;
196        }
197
198        max_compression_level = match self.file_format {
199            ARCHIVE_FORMAT_7ZIP => 9,
200            ARCHIVE_FORMAT_XAR => 9,
201            ARCHIVE_FORMAT_ZIP => 9,
202            _ => -1,
203        };
204
205        if max_compression_level > 0 {
206            self.add_format_option("compression-level", &max_compression_level.to_string())?;
207        }
208
209        Ok(())
210    }
211
212    pub fn set_compression_mid(&mut self) -> Result<()> {
213        let mut max_compression_level = match self.file_filter {
214            ARCHIVE_FILTER_BZIP2 => 9,
215            ARCHIVE_FILTER_GZIP => 9,
216            ARCHIVE_FILTER_LRZIP => 9,
217            ARCHIVE_FILTER_XZ => 9,
218            ARCHIVE_FILTER_LZ4 => 9,
219            ARCHIVE_FILTER_LZIP => 9,
220            ARCHIVE_FILTER_ZSTD => 22,
221            ARCHIVE_FILTER_LZMA => 9,
222            ARCHIVE_FILTER_LZOP => 9,
223            _ => -1,
224        };
225
226        if max_compression_level > 0 {
227            self.add_filter_option(
228                "compression-level",
229                &(max_compression_level / 2).to_string(),
230            )?;
231        }
232
233        max_compression_level = match self.file_format {
234            ARCHIVE_FORMAT_7ZIP => 9,
235            ARCHIVE_FORMAT_XAR => 9,
236            ARCHIVE_FORMAT_ZIP => 9,
237            _ => -1,
238        };
239
240        if max_compression_level > 0 {
241            self.add_format_option(
242                "compression-level",
243                &(max_compression_level / 2).to_string(),
244            )?;
245        }
246
247        Ok(())
248    }
249
250    pub fn set_compression_low(&mut self) -> Result<()> {
251        self.add_format_option("compression-level", "0")?;
252        self.add_filter_option("compression-level", "0")
253    }
254
255    // Simple Rust API. Nothing else but call new and then set format and add objects
256    // it cannot be simpler
257    pub fn set_output_targz(&mut self) -> Result<()> {
258        self.set_output_format(ARCHIVE_FORMAT_TAR)?;
259        self.set_output_filter(ARCHIVE_FILTER_GZIP)
260    }
261
262    pub fn set_output_tarxz(&mut self) -> Result<()> {
263        self.set_output_format(ARCHIVE_FORMAT_TAR)?;
264        self.set_output_filter(ARCHIVE_FILTER_XZ)
265    }
266
267    pub fn set_output_tarzst(&mut self) -> Result<()> {
268        self.set_output_format(ARCHIVE_FORMAT_TAR)?;
269        self.set_output_filter(ARCHIVE_FILTER_ZSTD)
270    }
271
272    pub fn set_output_7zlzma2(&mut self) -> Result<()> {
273        self.set_output_format(ARCHIVE_FORMAT_7ZIP)?;
274        self.set_output_filter(ARCHIVE_FILTER_NONE)?;
275        self.add_format_option("compression", "lzma2")
276    }
277
278    pub fn set_output_zip(&mut self) -> Result<()> {
279        self.set_output_format(ARCHIVE_FORMAT_ZIP)?;
280        self.set_output_filter(ARCHIVE_FILTER_NONE)?;
281        self.add_format_option("compression", "deflate")
282    }
283
284    pub fn add_obj_from_reader<S: Read>(
285        &mut self,
286        mut source: S,
287        archivepath: &str,
288        objmeta: &Metadata,
289    ) -> Result<()> {
290        let mut buffer: [u8; BUFFER_SIZE] = [0u8; BUFFER_SIZE];
291        let p = CString::new(archivepath.to_string()).unwrap();
292
293        unsafe {
294            let entry = archive_entry_new();
295
296            archive_entry_set_size(entry, objmeta.size()); // quick way to get the size?
297                                                           // archive_entry_set_perm(entry, 0o777);
298                                                           // archive_entry_set_filetype(entry, AE_IFREG);
299            archive_entry_set_mode(entry, objmeta.nodetype() | objmeta.perm());
300            archive_entry_set_perm(entry, objmeta.perm());
301            archive_entry_set_ctime(entry, objmeta.ctime(), objmeta.ctime_nano());
302            archive_entry_set_mtime(entry, objmeta.mtime(), objmeta.mtime_nano());
303            archive_entry_set_atime(entry, objmeta.atime(), objmeta.atime_nano());
304            archive_entry_set_pathname(entry, p.as_ptr());
305            archive_entry_set_uid(entry, objmeta.owner());
306            archive_entry_set_gid(entry, objmeta.group());
307
308            match archive_write_header(self.archive_writer, entry) {
309                carchive::ARCHIVE_OK | carchive::ARCHIVE_WARN => (),
310                _ => return Err(Error::from(self.archive_writer)),
311            }
312
313            loop {
314                let readed = source.read(&mut buffer)?;
315                if readed == 0 {
316                    break;
317                }
318
319                if archive_write_data(
320                    self.archive_writer,
321                    buffer.as_ptr() as *const c_void,
322                    readed,
323                ) != readed as isize
324                {
325                    return Err(Error::from(self.archive_writer));
326                }
327            }
328
329            //write file
330            archive_entry_free(entry);
331        }
332        Ok(())
333    }
334
335    /// Adds a file from the local filesystem to the archive.
336    pub fn add_file(&mut self, localpath: &str, archivepath: &str) -> Result<()> {
337        let source = File::open(localpath)?;
338        let meta = source.metadata()?;
339        self.add_obj_from_reader(source, archivepath, &meta.into())
340    }
341
342    /// Add an entry directly from an in-memory byte slice.
343    pub fn add_data(
344        &mut self,
345        archivepath: &str,
346        data: &[u8],
347        mtime: i64,
348        mtime_nano: i64,
349        is_dir: bool,
350    ) -> Result<()> {
351        let nodetype = if is_dir { crate::AE_IFDIR } else { crate::AE_IFREG };
352        let meta = crate::Metadata::from_fields(
353            data.len() as i64,
354            nodetype,
355            0o644,
356            mtime,
357            mtime_nano,
358        );
359        let cursor = std::io::Cursor::new(data);
360        self.add_obj_from_reader(cursor, archivepath, &meta)
361    }
362}
363
364impl<W: Write> Drop for ArchiveWriter<W> {
365    fn drop(&mut self) {
366        drop(self.free());
367    }
368}