Skip to main content

eazip/write/
mod.rs

1//! Utilities to write an archive.
2
3use crate::{
4    CompressionMethod, Timestamp,
5    compression::Compressor,
6    utils::{Counter, Crc32Writer},
7};
8use std::{fmt, io};
9
10mod raw;
11
12/// The options used when adding a file to an archive.
13///
14/// Setting the timestamp is not implemented yet.
15#[derive(Debug, Default, Clone)]
16#[non_exhaustive]
17pub struct FileOptions {
18    /// The compression method.
19    pub compression_method: CompressionMethod,
20    /// The compression level.
21    pub level: Option<i32>,
22    /// The modification time of the entry.
23    ///
24    /// Default value is `Timestamp::UNIX_EPOCH`, which means that this field is
25    /// ignored.
26    pub modified_at: Timestamp,
27}
28
29/// Wraps a writer to create a ZIP archive.
30///
31/// You need to call `self.finish()` when done.
32///
33/// When adding a file to the archive, some checks are made to ensure its name
34/// is valid (it is not absolute, does not contain the '\\' character, etc).
35/// Such validation checks may be added in a semver-compatible version if they
36/// may prevent invalid or dangerous archives.
37///
38/// # Example
39///
40/// ```no_run
41/// use std::io::prelude::*;
42///
43/// let mut archive = eazip::ArchiveWriter::create("example.zip")?;
44/// let options = eazip::write::FileOptions::default();
45///
46/// // Add a file
47/// archive.add_file("hello.txt", b"hello\n".as_slice(), &options)?;
48///
49/// // Add a directory
50/// archive.add_directory("dir/")?;
51///
52/// // Stream a file
53/// let mut writer = archive.stream_file("dir/streaming.txt", &options)?;
54/// writer.write_all(b"some data\n")?;
55/// writer.finish()?;
56///
57/// // Finish writing the archive
58/// archive.finish()?;
59/// # Ok::<(), std::io::Error>(())
60/// ```
61#[derive(Debug, Default)]
62pub struct ArchiveWriter<W: io::Write> {
63    writer: W,
64    raw: raw::RawArchiveWriter,
65}
66
67impl ArchiveWriter<std::fs::File> {
68    /// Creates a new `ArchiveWriter` that writes to the given file.
69    ///
70    /// The file will be created if it does not exist, and will be truncated if
71    /// it does.
72    pub fn create(path: impl AsRef<std::path::Path>) -> io::Result<Self> {
73        std::fs::File::create(path).map(Self::new)
74    }
75
76    /// Creates a new `ArchiveWriter` that writes to the given file; error if
77    /// the file exists.
78    pub fn create_new(path: impl AsRef<std::path::Path>) -> io::Result<Self> {
79        std::fs::File::create_new(path).map(Self::new)
80    }
81}
82
83impl<W: io::Write> ArchiveWriter<W> {
84    /// Creates a new `ArchiveWriter` that writes to the given writer.
85    #[inline]
86    pub fn new(writer: W) -> Self {
87        ArchiveWriter {
88            writer,
89            raw: raw::RawArchiveWriter::default(),
90        }
91    }
92
93    /// Writes a file to the archive.
94    ///
95    /// The entire compressed content of the file must fit in memory.
96    pub fn add_file<R: io::Read>(
97        &mut self,
98        name: &str,
99        mut content: R,
100        options: &FileOptions,
101    ) -> io::Result<()> {
102        let mut w = Crc32Writer::new(Compressor::new(
103            Vec::new(),
104            options.compression_method,
105            options.level,
106        )?);
107        let uncompressed_size = io::copy(&mut content, &mut w)?;
108        let crc32 = w.result();
109        let compressed = w.into_inner().finish()?;
110
111        self.raw.write_file_raw(
112            &mut self.writer,
113            name,
114            &compressed,
115            &raw::Metadata {
116                compression_method: options.compression_method,
117                compressed_size: compressed.len() as _,
118                uncompressed_size,
119                crc32,
120                typ: crate::FileType::File,
121                modified_at: options.modified_at,
122            },
123            "",
124        )?;
125
126        Ok(())
127    }
128
129    /// Starts streaming a file to the archive.
130    ///
131    /// This is useful for (but not limited to) very large files that may not
132    /// fit in memory.
133    ///
134    /// This method returns a `FileStreamer` that can be written to.
135    pub fn stream_file(
136        &mut self,
137        name: &str,
138        options: &FileOptions,
139    ) -> io::Result<FileStreamer<'_, W>> {
140        let writer = self.raw.start_stream_raw(&mut self.writer, name, options)?;
141
142        Ok(FileStreamer {
143            writer: Counter::new(Crc32Writer::new(Compressor::new(
144                writer,
145                options.compression_method,
146                options.level,
147            )?)),
148        })
149    }
150
151    /// Adds a directory to the archive.
152    pub fn add_directory(&mut self, name: &str) -> io::Result<()> {
153        self.raw.write_file_raw(
154            &mut self.writer,
155            name,
156            &[],
157            &raw::Metadata {
158                compression_method: CompressionMethod::STORE,
159                compressed_size: 0,
160                uncompressed_size: 0,
161                crc32: 0,
162                typ: crate::FileType::Directory,
163                modified_at: Timestamp::UNIX_EPOCH,
164            },
165            "",
166        )
167    }
168
169    /// Adds a symlink to the archive.
170    ///
171    /// The target of the symlink is not validated yet, which may be used to
172    /// create dangerous archives if used with untrusted input. This will be
173    /// fixed in a future version so this behaviour should not be relied on.
174    pub fn add_symlink(&mut self, name: &str, target: &str) -> io::Result<()> {
175        self.raw.write_file_raw(
176            &mut self.writer,
177            name,
178            target.as_bytes(),
179            &raw::Metadata {
180                compression_method: CompressionMethod::STORE,
181                compressed_size: target.len() as _,
182                uncompressed_size: target.len() as _,
183                crc32: crc32fast::hash(target.as_bytes()),
184                typ: crate::FileType::Symlink,
185                modified_at: Timestamp::UNIX_EPOCH,
186            },
187            "",
188        )
189    }
190
191    /// Tries to recover from an error by erasing the last entry.
192    ///
193    /// Note that this requires a seeking writer. Calling this when no error
194    /// needs recovery does nothing.
195    ///
196    /// **Footgun**: this requires the user to properly truncate the writer after
197    /// using this method.
198    #[inline]
199    pub fn recover(&mut self) -> io::Result<()>
200    where
201        W: io::Seek,
202    {
203        self.raw.recover(&mut self.writer)
204    }
205
206    /// Gets a shared reference to the underlying writer.
207    #[inline]
208    pub fn get_ref(&self) -> &W {
209        &self.writer
210    }
211
212    /// Gets a mutable reference to the underlying writer.
213    ///
214    /// It is inadvisable to directly write to the underlying writer.
215    #[inline]
216    pub fn get_mut(&mut self) -> &mut W {
217        &mut self.writer
218    }
219
220    /// Flushes the underlying stream.
221    #[inline]
222    pub fn flush(&mut self) -> io::Result<()> {
223        self.writer.flush()
224    }
225
226    /// Finishes writing the archive and get the writer back.
227    ///
228    /// It is necessary to call this method or the resulting archive will not
229    /// be readable.
230    #[inline]
231    pub fn finish(mut self) -> io::Result<W> {
232        self.raw.finish(&mut self.writer)?;
233        Ok(self.writer)
234    }
235}
236
237/// An adapter to stream a ZIP file.
238///
239/// Writing to this value will write to a file in an archive.
240///
241/// It is necessary to call `finish` when done.
242pub struct FileStreamer<'a, W: io::Write> {
243    writer: Counter<Crc32Writer<Compressor<raw::RawFileStreamer<'a, &'a mut W>>>>,
244}
245
246impl<W: io::Write> io::Write for FileStreamer<'_, W> {
247    #[inline]
248    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
249        self.writer.write(buf)
250    }
251
252    #[inline]
253    fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<usize> {
254        self.writer.write_vectored(bufs)
255    }
256
257    #[inline]
258    fn flush(&mut self) -> io::Result<()> {
259        self.writer.flush()
260    }
261}
262
263impl<W: io::Write> FileStreamer<'_, W> {
264    /// Finishes writing the current file.
265    pub fn finish(self) -> io::Result<()> {
266        self.finish_with_comment("")
267    }
268
269    /// Finishes writing the current file.
270    pub fn finish_with_comment(self, comment: &str) -> io::Result<()> {
271        let uncompressed_size = self.writer.amt;
272        let crc32 = self.writer.inner.result();
273
274        let raw_writer = self.writer.inner.into_inner().finish()?;
275
276        raw_writer.finish(uncompressed_size, crc32, comment)
277    }
278}
279
280impl<'a, W: io::Write + fmt::Debug> fmt::Debug for FileStreamer<'a, W> {
281    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282        f.write_str("FileStreamer { .. }")
283    }
284}