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