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        Ok(())
126    }
127
128    /// Starts streaming a file to the archive.
129    ///
130    /// This is useful for (but not limited to) very large files that may not
131    /// fit in memory.
132    ///
133    /// This method returns a `FileStreamer` that can be written to.
134    pub fn stream_file(
135        &mut self,
136        name: &str,
137        options: &FileOptions,
138    ) -> io::Result<FileStreamer<'_, W>> {
139        let writer = self.raw.start_stream_raw(&mut self.writer, name, options)?;
140
141        Ok(FileStreamer {
142            writer: Counter::new(Crc32Writer::new(Compressor::new(
143                writer,
144                options.compression_method,
145                options.level,
146            )?)),
147        })
148    }
149
150    /// Adds a directory to the archive.
151    pub fn add_directory(&mut self, name: &str) -> io::Result<()> {
152        if !name.ends_with('/') {
153            return Err(io::Error::new(
154                io::ErrorKind::InvalidInput,
155                "directory name must end with '/'",
156            ));
157        }
158
159        self.raw.write_file_raw(
160            &mut self.writer,
161            name,
162            &[],
163            &raw::Metadata {
164                compression_method: CompressionMethod::STORE,
165                compressed_size: 0,
166                uncompressed_size: 0,
167                crc32: 0,
168                typ: crate::FileType::Directory,
169                modified_at: Timestamp::UNIX_EPOCH,
170            },
171        )
172    }
173
174    /// Adds a symlink to the archive.
175    ///
176    /// The target of the symlink is not validated yet, which may be used to
177    /// create dangerous archives if used with untrusted input. This will be
178    /// fixed in a future version so this behaviour should not be relied on.
179    pub fn add_symlink(&mut self, name: &str, target: &str) -> io::Result<()> {
180        self.raw.write_file_raw(
181            &mut self.writer,
182            name,
183            target.as_bytes(),
184            &raw::Metadata {
185                compression_method: CompressionMethod::STORE,
186                compressed_size: target.len() as _,
187                uncompressed_size: target.len() as _,
188                crc32: crc32fast::hash(target.as_bytes()),
189                typ: crate::FileType::Symlink,
190                modified_at: Timestamp::UNIX_EPOCH,
191            },
192        )
193    }
194
195    /// Tries to recover from an error by erasing the last entry.
196    ///
197    /// Note that this requires a seeking writer. Calling this when no error
198    /// needs recovery does nothing.
199    ///
200    /// **Footgun**: this requires the user to properly truncate the writer after
201    /// using this method.
202    #[inline]
203    pub fn recover(&mut self) -> io::Result<()>
204    where
205        W: io::Seek,
206    {
207        self.raw.recover(&mut self.writer)
208    }
209
210    /// Gets a shared reference to the underlying writer.
211    #[inline]
212    pub fn get_ref(&self) -> &W {
213        &self.writer
214    }
215
216    /// Gets a mutable reference to the underlying writer.
217    ///
218    /// It is inadvisable to directly write to the underlying writer.
219    #[inline]
220    pub fn get_mut(&mut self) -> &mut W {
221        &mut self.writer
222    }
223
224    /// Flushes the underlying stream.
225    #[inline]
226    pub fn flush(&mut self) -> io::Result<()> {
227        self.writer.flush()
228    }
229
230    /// Finishes writing the archive and get the writer back.
231    ///
232    /// It is necessary to call this method or the resulting archive will not
233    /// be readable.
234    #[inline]
235    pub fn finish(mut self) -> io::Result<W> {
236        self.raw.finish(&mut self.writer)?;
237        Ok(self.writer)
238    }
239}
240
241/// An adapter to stream a ZIP file.
242///
243/// Writing to this value will write to a file in an archive.
244///
245/// It is necessary to call `finish` when done.
246pub struct FileStreamer<'a, W: io::Write> {
247    writer: Counter<Crc32Writer<Compressor<raw::RawFileStreamer<'a, &'a mut W>>>>,
248}
249
250impl<W: io::Write> io::Write for FileStreamer<'_, W> {
251    #[inline]
252    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
253        self.writer.write(buf)
254    }
255
256    #[inline]
257    fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<usize> {
258        self.writer.write_vectored(bufs)
259    }
260
261    #[inline]
262    fn flush(&mut self) -> io::Result<()> {
263        self.writer.flush()
264    }
265}
266
267impl<W: io::Write> FileStreamer<'_, W> {
268    /// Finishes writing the current file.
269    pub fn finish(self) -> io::Result<()> {
270        let uncompressed_size = self.writer.amt;
271        let crc32 = self.writer.inner.result();
272
273        let raw_writer = self.writer.inner.into_inner().finish()?;
274
275        raw_writer.finish(uncompressed_size, crc32)
276    }
277}
278
279impl<'a, W: io::Write + fmt::Debug> fmt::Debug for FileStreamer<'a, W> {
280    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281        f.write_str("FileStreamer { .. }")
282    }
283}