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}