1//! Types for creating ZIP archives
2
3use crate::compression::CompressionMethod;
4use crate::read::{central_header_to_zip_file, ZipArchive, ZipFile};
5use crate::result::{ZipError, ZipResult};
6use crate::spec;
7use crate::types::{AtomicU64, DateTime, System, ZipFileData, DEFAULT_VERSION};
8use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
9use crc32fast::Hasher;
10use std::convert::TryInto;
11use std::default::Default;
12use std::io;
13use std::io::prelude::*;
14use std::mem;
15
16#[cfg(any(
17 feature = "deflate",
18 feature = "deflate-miniz",
19 feature = "deflate-zlib"
20))]
21use flate2::write::DeflateEncoder;
22
23#[cfg(feature = "bzip2")]
24use bzip2::write::BzEncoder;
25
26#[cfg(feature = "time")]
27use time::OffsetDateTime;
28
29#[cfg(feature = "zstd")]
30use zstd::stream::write::Encoder as ZstdEncoder;
31
32enum MaybeEncrypted<W> {
33 Unencrypted(W),
34 Encrypted(crate::zipcrypto::ZipCryptoWriter<W>),
35}
36impl<W: Write> Write for MaybeEncrypted<W> {
37 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
38 match self {
39 MaybeEncrypted::Unencrypted(w: &mut W) => w.write(buf),
40 MaybeEncrypted::Encrypted(w: &mut ZipCryptoWriter) => w.write(buf),
41 }
42 }
43 fn flush(&mut self) -> io::Result<()> {
44 match self {
45 MaybeEncrypted::Unencrypted(w: &mut W) => w.flush(),
46 MaybeEncrypted::Encrypted(w: &mut ZipCryptoWriter) => w.flush(),
47 }
48 }
49}
50enum GenericZipWriter<W: Write + io::Seek> {
51 Closed,
52 Storer(MaybeEncrypted<W>),
53 #[cfg(any(
54 feature = "deflate",
55 feature = "deflate-miniz",
56 feature = "deflate-zlib"
57 ))]
58 Deflater(DeflateEncoder<MaybeEncrypted<W>>),
59 #[cfg(feature = "bzip2")]
60 Bzip2(BzEncoder<MaybeEncrypted<W>>),
61 #[cfg(feature = "zstd")]
62 Zstd(ZstdEncoder<'static, MaybeEncrypted<W>>),
63}
64// Put the struct declaration in a private module to convince rustdoc to display ZipWriter nicely
65pub(crate) mod zip_writer {
66 use super::*;
67 /// ZIP archive generator
68 ///
69 /// Handles the bookkeeping involved in building an archive, and provides an
70 /// API to edit its contents.
71 ///
72 /// ```
73 /// # fn doit() -> zip::result::ZipResult<()>
74 /// # {
75 /// # use zip::ZipWriter;
76 /// use std::io::Write;
77 /// use zip::write::FileOptions;
78 ///
79 /// // We use a buffer here, though you'd normally use a `File`
80 /// let mut buf = [0; 65536];
81 /// let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf[..]));
82 ///
83 /// let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
84 /// zip.start_file("hello_world.txt", options)?;
85 /// zip.write(b"Hello, World!")?;
86 ///
87 /// // Apply the changes you've made.
88 /// // Dropping the `ZipWriter` will have the same effect, but may silently fail
89 /// zip.finish()?;
90 ///
91 /// # Ok(())
92 /// # }
93 /// # doit().unwrap();
94 /// ```
95 pub struct ZipWriter<W: Write + io::Seek> {
96 pub(super) inner: GenericZipWriter<W>,
97 pub(super) files: Vec<ZipFileData>,
98 pub(super) stats: ZipWriterStats,
99 pub(super) writing_to_file: bool,
100 pub(super) writing_to_extra_field: bool,
101 pub(super) writing_to_central_extra_field_only: bool,
102 pub(super) writing_raw: bool,
103 pub(super) comment: Vec<u8>,
104 }
105}
106pub use zip_writer::ZipWriter;
107
108#[derive(Default)]
109struct ZipWriterStats {
110 hasher: Hasher,
111 start: u64,
112 bytes_written: u64,
113}
114
115struct ZipRawValues {
116 crc32: u32,
117 compressed_size: u64,
118 uncompressed_size: u64,
119}
120
121/// Metadata for a file to be written
122#[derive(Copy, Clone)]
123pub struct FileOptions {
124 compression_method: CompressionMethod,
125 compression_level: Option<i32>,
126 last_modified_time: DateTime,
127 permissions: Option<u32>,
128 large_file: bool,
129 encrypt_with: Option<crate::zipcrypto::ZipCryptoKeys>,
130}
131
132impl FileOptions {
133 /// Set the compression method for the new file
134 ///
135 /// The default is `CompressionMethod::Deflated`. If the deflate compression feature is
136 /// disabled, `CompressionMethod::Stored` becomes the default.
137 #[must_use]
138 pub fn compression_method(mut self, method: CompressionMethod) -> FileOptions {
139 self.compression_method = method;
140 self
141 }
142
143 /// Set the compression level for the new file
144 ///
145 /// `None` value specifies default compression level.
146 ///
147 /// Range of values depends on compression method:
148 /// * `Deflated`: 0 - 9. Default is 6
149 /// * `Bzip2`: 0 - 9. Default is 6
150 /// * `Zstd`: -7 - 22, with zero being mapped to default level. Default is 3
151 /// * others: only `None` is allowed
152 #[must_use]
153 pub fn compression_level(mut self, level: Option<i32>) -> FileOptions {
154 self.compression_level = level;
155 self
156 }
157
158 /// Set the last modified time
159 ///
160 /// The default is the current timestamp if the 'time' feature is enabled, and 1980-01-01
161 /// otherwise
162 #[must_use]
163 pub fn last_modified_time(mut self, mod_time: DateTime) -> FileOptions {
164 self.last_modified_time = mod_time;
165 self
166 }
167
168 /// Set the permissions for the new file.
169 ///
170 /// The format is represented with unix-style permissions.
171 /// The default is `0o644`, which represents `rw-r--r--` for files,
172 /// and `0o755`, which represents `rwxr-xr-x` for directories.
173 ///
174 /// This method only preserves the file permissions bits (via a `& 0o777`) and discards
175 /// higher file mode bits. So it cannot be used to denote an entry as a directory,
176 /// symlink, or other special file type.
177 #[must_use]
178 pub fn unix_permissions(mut self, mode: u32) -> FileOptions {
179 self.permissions = Some(mode & 0o777);
180 self
181 }
182
183 /// Set whether the new file's compressed and uncompressed size is less than 4 GiB.
184 ///
185 /// If set to `false` and the file exceeds the limit, an I/O error is thrown. If set to `true`,
186 /// readers will require ZIP64 support and if the file does not exceed the limit, 20 B are
187 /// wasted. The default is `false`.
188 #[must_use]
189 pub fn large_file(mut self, large: bool) -> FileOptions {
190 self.large_file = large;
191 self
192 }
193 pub(crate) fn with_deprecated_encryption(mut self, password: &[u8]) -> FileOptions {
194 self.encrypt_with = Some(crate::zipcrypto::ZipCryptoKeys::derive(password));
195 self
196 }
197}
198
199impl Default for FileOptions {
200 /// Construct a new FileOptions object
201 fn default() -> Self {
202 Self {
203 #[cfg(any(
204 feature = "deflate",
205 feature = "deflate-miniz",
206 feature = "deflate-zlib"
207 ))]
208 compression_method: CompressionMethod::Deflated,
209 #[cfg(not(any(
210 feature = "deflate",
211 feature = "deflate-miniz",
212 feature = "deflate-zlib"
213 )))]
214 compression_method: CompressionMethod::Stored,
215 compression_level: None,
216 #[cfg(feature = "time")]
217 last_modified_time: OffsetDateTime::now_utc().try_into().unwrap_or_default(),
218 #[cfg(not(feature = "time"))]
219 last_modified_time: DateTime::default(),
220 permissions: None,
221 large_file: false,
222 encrypt_with: None,
223 }
224 }
225}
226
227impl<W: Write + io::Seek> Write for ZipWriter<W> {
228 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
229 if !self.writing_to_file {
230 return Err(io::Error::new(
231 io::ErrorKind::Other,
232 "No file has been started",
233 ));
234 }
235 match self.inner.ref_mut() {
236 Some(ref mut w) => {
237 if self.writing_to_extra_field {
238 self.files.last_mut().unwrap().extra_field.write(buf)
239 } else {
240 let write_result = w.write(buf);
241 if let Ok(count) = write_result {
242 self.stats.update(&buf[0..count]);
243 if self.stats.bytes_written > spec::ZIP64_BYTES_THR
244 && !self.files.last_mut().unwrap().large_file
245 {
246 let _inner = mem::replace(&mut self.inner, GenericZipWriter::Closed);
247 return Err(io::Error::new(
248 io::ErrorKind::Other,
249 "Large file option has not been set",
250 ));
251 }
252 }
253 write_result
254 }
255 }
256 None => Err(io::Error::new(
257 io::ErrorKind::BrokenPipe,
258 "ZipWriter was already closed",
259 )),
260 }
261 }
262
263 fn flush(&mut self) -> io::Result<()> {
264 match self.inner.ref_mut() {
265 Some(ref mut w) => w.flush(),
266 None => Err(io::Error::new(
267 io::ErrorKind::BrokenPipe,
268 "ZipWriter was already closed",
269 )),
270 }
271 }
272}
273
274impl ZipWriterStats {
275 fn update(&mut self, buf: &[u8]) {
276 self.hasher.update(buf);
277 self.bytes_written += buf.len() as u64;
278 }
279}
280
281impl<A: Read + Write + io::Seek> ZipWriter<A> {
282 /// Initializes the archive from an existing ZIP archive, making it ready for append.
283 pub fn new_append(mut readwriter: A) -> ZipResult<ZipWriter<A>> {
284 let (footer, cde_start_pos) = spec::CentralDirectoryEnd::find_and_parse(&mut readwriter)?;
285
286 if footer.disk_number != footer.disk_with_central_directory {
287 return Err(ZipError::UnsupportedArchive(
288 "Support for multi-disk files is not implemented",
289 ));
290 }
291
292 let (archive_offset, directory_start, number_of_files) =
293 ZipArchive::get_directory_counts(&mut readwriter, &footer, cde_start_pos)?;
294
295 if readwriter
296 .seek(io::SeekFrom::Start(directory_start))
297 .is_err()
298 {
299 return Err(ZipError::InvalidArchive(
300 "Could not seek to start of central directory",
301 ));
302 }
303
304 let files = (0..number_of_files)
305 .map(|_| central_header_to_zip_file(&mut readwriter, archive_offset))
306 .collect::<Result<Vec<_>, _>>()?;
307
308 let _ = readwriter.seek(io::SeekFrom::Start(directory_start)); // seek directory_start to overwrite it
309
310 Ok(ZipWriter {
311 inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(readwriter)),
312 files,
313 stats: Default::default(),
314 writing_to_file: false,
315 writing_to_extra_field: false,
316 writing_to_central_extra_field_only: false,
317 comment: footer.zip_file_comment,
318 writing_raw: true, // avoid recomputing the last file's header
319 })
320 }
321}
322
323impl<W: Write + io::Seek> ZipWriter<W> {
324 /// Initializes the archive.
325 ///
326 /// Before writing to this object, the [`ZipWriter::start_file`] function should be called.
327 pub fn new(inner: W) -> ZipWriter<W> {
328 ZipWriter {
329 inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(inner)),
330 files: Vec::new(),
331 stats: Default::default(),
332 writing_to_file: false,
333 writing_to_extra_field: false,
334 writing_to_central_extra_field_only: false,
335 writing_raw: false,
336 comment: Vec::new(),
337 }
338 }
339
340 /// Set ZIP archive comment.
341 pub fn set_comment<S>(&mut self, comment: S)
342 where
343 S: Into<String>,
344 {
345 self.set_raw_comment(comment.into().into())
346 }
347
348 /// Set ZIP archive comment.
349 ///
350 /// This sets the raw bytes of the comment. The comment
351 /// is typically expected to be encoded in UTF-8
352 pub fn set_raw_comment(&mut self, comment: Vec<u8>) {
353 self.comment = comment;
354 }
355
356 /// Start a new file for with the requested options.
357 fn start_entry<S>(
358 &mut self,
359 name: S,
360 options: FileOptions,
361 raw_values: Option<ZipRawValues>,
362 ) -> ZipResult<()>
363 where
364 S: Into<String>,
365 {
366 self.finish_file()?;
367
368 let raw_values = raw_values.unwrap_or(ZipRawValues {
369 crc32: 0,
370 compressed_size: 0,
371 uncompressed_size: 0,
372 });
373
374 {
375 let writer = self.inner.get_plain();
376 let header_start = writer.stream_position()?;
377
378 let permissions = options.permissions.unwrap_or(0o100644);
379 let mut file = ZipFileData {
380 system: System::Unix,
381 version_made_by: DEFAULT_VERSION,
382 encrypted: options.encrypt_with.is_some(),
383 using_data_descriptor: false,
384 compression_method: options.compression_method,
385 compression_level: options.compression_level,
386 last_modified_time: options.last_modified_time,
387 crc32: raw_values.crc32,
388 compressed_size: raw_values.compressed_size,
389 uncompressed_size: raw_values.uncompressed_size,
390 file_name: name.into(),
391 file_name_raw: Vec::new(), // Never used for saving
392 extra_field: Vec::new(),
393 file_comment: String::new(),
394 header_start,
395 data_start: AtomicU64::new(0),
396 central_header_start: 0,
397 external_attributes: permissions << 16,
398 large_file: options.large_file,
399 aes_mode: None,
400 };
401 write_local_file_header(writer, &file)?;
402
403 let header_end = writer.stream_position()?;
404 self.stats.start = header_end;
405 *file.data_start.get_mut() = header_end;
406
407 self.stats.bytes_written = 0;
408 self.stats.hasher = Hasher::new();
409
410 self.files.push(file);
411 }
412 if let Some(keys) = options.encrypt_with {
413 let mut zipwriter = crate::zipcrypto::ZipCryptoWriter { writer: core::mem::replace(&mut self.inner, GenericZipWriter::Closed).unwrap(), buffer: vec![], keys };
414 let mut crypto_header = [0u8; 12];
415
416 zipwriter.write_all(&crypto_header)?;
417 self.inner = GenericZipWriter::Storer(MaybeEncrypted::Encrypted(zipwriter));
418 }
419 Ok(())
420 }
421
422 fn finish_file(&mut self) -> ZipResult<()> {
423 if self.writing_to_extra_field {
424 // Implicitly calling [`ZipWriter::end_extra_data`] for empty files.
425 self.end_extra_data()?;
426 }
427 self.inner.switch_to(CompressionMethod::Stored, None)?;
428 match core::mem::replace(&mut self.inner, GenericZipWriter::Closed) {
429 GenericZipWriter::Storer(MaybeEncrypted::Encrypted(writer)) => {
430 let crc32 = self.stats.hasher.clone().finalize();
431 self.inner = GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(writer.finish(crc32)?))
432 }
433 GenericZipWriter::Storer(w) => self.inner = GenericZipWriter::Storer(w),
434 _ => unreachable!()
435 }
436 let writer = self.inner.get_plain();
437
438 if !self.writing_raw {
439 let file = match self.files.last_mut() {
440 None => return Ok(()),
441 Some(f) => f,
442 };
443 file.crc32 = self.stats.hasher.clone().finalize();
444 file.uncompressed_size = self.stats.bytes_written;
445
446 let file_end = writer.stream_position()?;
447 file.compressed_size = file_end - self.stats.start;
448
449 update_local_file_header(writer, file)?;
450 writer.seek(io::SeekFrom::Start(file_end))?;
451 }
452
453 self.writing_to_file = false;
454 self.writing_raw = false;
455 Ok(())
456 }
457
458 /// Create a file in the archive and start writing its' contents.
459 ///
460 /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`]
461 pub fn start_file<S>(&mut self, name: S, mut options: FileOptions) -> ZipResult<()>
462 where
463 S: Into<String>,
464 {
465 if options.permissions.is_none() {
466 options.permissions = Some(0o644);
467 }
468 *options.permissions.as_mut().unwrap() |= 0o100000;
469 self.start_entry(name, options, None)?;
470 self.inner
471 .switch_to(options.compression_method, options.compression_level)?;
472 self.writing_to_file = true;
473 Ok(())
474 }
475
476 /// Starts a file, taking a Path as argument.
477 ///
478 /// This function ensures that the '/' path separator is used. It also ignores all non 'Normal'
479 /// Components, such as a starting '/' or '..' and '.'.
480 #[deprecated(
481 since = "0.5.7",
482 note = "by stripping `..`s from the path, the meaning of paths can change. Use `start_file` instead."
483 )]
484 pub fn start_file_from_path(
485 &mut self,
486 path: &std::path::Path,
487 options: FileOptions,
488 ) -> ZipResult<()> {
489 self.start_file(path_to_string(path), options)
490 }
491
492 /// Create an aligned file in the archive and start writing its' contents.
493 ///
494 /// Returns the number of padding bytes required to align the file.
495 ///
496 /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`]
497 pub fn start_file_aligned<S>(
498 &mut self,
499 name: S,
500 options: FileOptions,
501 align: u16,
502 ) -> Result<u64, ZipError>
503 where
504 S: Into<String>,
505 {
506 let data_start = self.start_file_with_extra_data(name, options)?;
507 let align = align as u64;
508 if align > 1 && data_start % align != 0 {
509 let pad_length = (align - (data_start + 4) % align) % align;
510 let pad = vec![0; pad_length as usize];
511 self.write_all(b"za").map_err(ZipError::from)?; // 0x617a
512 self.write_u16::<LittleEndian>(pad.len() as u16)
513 .map_err(ZipError::from)?;
514 self.write_all(&pad).map_err(ZipError::from)?;
515 assert_eq!(self.end_local_start_central_extra_data()? % align, 0);
516 }
517 let extra_data_end = self.end_extra_data()?;
518 Ok(extra_data_end - data_start)
519 }
520
521 /// Create a file in the archive and start writing its extra data first.
522 ///
523 /// Finish writing extra data and start writing file data with [`ZipWriter::end_extra_data`].
524 /// Optionally, distinguish local from central extra data with
525 /// [`ZipWriter::end_local_start_central_extra_data`].
526 ///
527 /// Returns the preliminary starting offset of the file data without any extra data allowing to
528 /// align the file data by calculating a pad length to be prepended as part of the extra data.
529 ///
530 /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`]
531 ///
532 /// ```
533 /// use byteorder::{LittleEndian, WriteBytesExt};
534 /// use zip::{ZipArchive, ZipWriter, result::ZipResult};
535 /// use zip::{write::FileOptions, CompressionMethod};
536 /// use std::io::{Write, Cursor};
537 ///
538 /// # fn main() -> ZipResult<()> {
539 /// let mut archive = Cursor::new(Vec::new());
540 ///
541 /// {
542 /// let mut zip = ZipWriter::new(&mut archive);
543 /// let options = FileOptions::default()
544 /// .compression_method(CompressionMethod::Stored);
545 ///
546 /// zip.start_file_with_extra_data("identical_extra_data.txt", options)?;
547 /// let extra_data = b"local and central extra data";
548 /// zip.write_u16::<LittleEndian>(0xbeef)?;
549 /// zip.write_u16::<LittleEndian>(extra_data.len() as u16)?;
550 /// zip.write_all(extra_data)?;
551 /// zip.end_extra_data()?;
552 /// zip.write_all(b"file data")?;
553 ///
554 /// let data_start = zip.start_file_with_extra_data("different_extra_data.txt", options)?;
555 /// let extra_data = b"local extra data";
556 /// zip.write_u16::<LittleEndian>(0xbeef)?;
557 /// zip.write_u16::<LittleEndian>(extra_data.len() as u16)?;
558 /// zip.write_all(extra_data)?;
559 /// let data_start = data_start as usize + 4 + extra_data.len() + 4;
560 /// let align = 64;
561 /// let pad_length = (align - data_start % align) % align;
562 /// assert_eq!(pad_length, 19);
563 /// zip.write_u16::<LittleEndian>(0xdead)?;
564 /// zip.write_u16::<LittleEndian>(pad_length as u16)?;
565 /// zip.write_all(&vec![0; pad_length])?;
566 /// let data_start = zip.end_local_start_central_extra_data()?;
567 /// assert_eq!(data_start as usize % align, 0);
568 /// let extra_data = b"central extra data";
569 /// zip.write_u16::<LittleEndian>(0xbeef)?;
570 /// zip.write_u16::<LittleEndian>(extra_data.len() as u16)?;
571 /// zip.write_all(extra_data)?;
572 /// zip.end_extra_data()?;
573 /// zip.write_all(b"file data")?;
574 ///
575 /// zip.finish()?;
576 /// }
577 ///
578 /// let mut zip = ZipArchive::new(archive)?;
579 /// assert_eq!(&zip.by_index(0)?.extra_data()[4..], b"local and central extra data");
580 /// assert_eq!(&zip.by_index(1)?.extra_data()[4..], b"central extra data");
581 /// # Ok(())
582 /// # }
583 /// ```
584 pub fn start_file_with_extra_data<S>(
585 &mut self,
586 name: S,
587 mut options: FileOptions,
588 ) -> ZipResult<u64>
589 where
590 S: Into<String>,
591 {
592 if options.permissions.is_none() {
593 options.permissions = Some(0o644);
594 }
595 *options.permissions.as_mut().unwrap() |= 0o100000;
596 self.start_entry(name, options, None)?;
597 self.writing_to_file = true;
598 self.writing_to_extra_field = true;
599 Ok(self.files.last().unwrap().data_start.load())
600 }
601
602 /// End local and start central extra data. Requires [`ZipWriter::start_file_with_extra_data`].
603 ///
604 /// Returns the final starting offset of the file data.
605 pub fn end_local_start_central_extra_data(&mut self) -> ZipResult<u64> {
606 let data_start = self.end_extra_data()?;
607 self.files.last_mut().unwrap().extra_field.clear();
608 self.writing_to_extra_field = true;
609 self.writing_to_central_extra_field_only = true;
610 Ok(data_start)
611 }
612
613 /// End extra data and start file data. Requires [`ZipWriter::start_file_with_extra_data`].
614 ///
615 /// Returns the final starting offset of the file data.
616 pub fn end_extra_data(&mut self) -> ZipResult<u64> {
617 // Require `start_file_with_extra_data()`. Ensures `file` is some.
618 if !self.writing_to_extra_field {
619 return Err(ZipError::Io(io::Error::new(
620 io::ErrorKind::Other,
621 "Not writing to extra field",
622 )));
623 }
624 let file = self.files.last_mut().unwrap();
625
626 validate_extra_data(file)?;
627
628 let data_start = file.data_start.get_mut();
629
630 if !self.writing_to_central_extra_field_only {
631 let writer = self.inner.get_plain();
632
633 // Append extra data to local file header and keep it for central file header.
634 writer.write_all(&file.extra_field)?;
635
636 // Update final `data_start`.
637 let header_end = *data_start + file.extra_field.len() as u64;
638 self.stats.start = header_end;
639 *data_start = header_end;
640
641 // Update extra field length in local file header.
642 let extra_field_length =
643 if file.large_file { 20 } else { 0 } + file.extra_field.len() as u16;
644 writer.seek(io::SeekFrom::Start(file.header_start + 28))?;
645 writer.write_u16::<LittleEndian>(extra_field_length)?;
646 writer.seek(io::SeekFrom::Start(header_end))?;
647
648 self.inner
649 .switch_to(file.compression_method, file.compression_level)?;
650 }
651
652 self.writing_to_extra_field = false;
653 self.writing_to_central_extra_field_only = false;
654 Ok(*data_start)
655 }
656
657 /// Add a new file using the already compressed data from a ZIP file being read and renames it, this
658 /// allows faster copies of the `ZipFile` since there is no need to decompress and compress it again.
659 /// Any `ZipFile` metadata is copied and not checked, for example the file CRC.
660
661 /// ```no_run
662 /// use std::fs::File;
663 /// use std::io::{Read, Seek, Write};
664 /// use zip::{ZipArchive, ZipWriter};
665 ///
666 /// fn copy_rename<R, W>(
667 /// src: &mut ZipArchive<R>,
668 /// dst: &mut ZipWriter<W>,
669 /// ) -> zip::result::ZipResult<()>
670 /// where
671 /// R: Read + Seek,
672 /// W: Write + Seek,
673 /// {
674 /// // Retrieve file entry by name
675 /// let file = src.by_name("src_file.txt")?;
676 ///
677 /// // Copy and rename the previously obtained file entry to the destination zip archive
678 /// dst.raw_copy_file_rename(file, "new_name.txt")?;
679 ///
680 /// Ok(())
681 /// }
682 /// ```
683 pub fn raw_copy_file_rename<S>(&mut self, mut file: ZipFile, name: S) -> ZipResult<()>
684 where
685 S: Into<String>,
686 {
687 let mut options = FileOptions::default()
688 .large_file(file.compressed_size().max(file.size()) > spec::ZIP64_BYTES_THR)
689 .last_modified_time(file.last_modified())
690 .compression_method(file.compression());
691 if let Some(perms) = file.unix_mode() {
692 options = options.unix_permissions(perms);
693 }
694
695 let raw_values = ZipRawValues {
696 crc32: file.crc32(),
697 compressed_size: file.compressed_size(),
698 uncompressed_size: file.size(),
699 };
700
701 self.start_entry(name, options, Some(raw_values))?;
702 self.writing_to_file = true;
703 self.writing_raw = true;
704
705 io::copy(file.get_raw_reader(), self)?;
706
707 Ok(())
708 }
709
710 /// Add a new file using the already compressed data from a ZIP file being read, this allows faster
711 /// copies of the `ZipFile` since there is no need to decompress and compress it again. Any `ZipFile`
712 /// metadata is copied and not checked, for example the file CRC.
713 ///
714 /// ```no_run
715 /// use std::fs::File;
716 /// use std::io::{Read, Seek, Write};
717 /// use zip::{ZipArchive, ZipWriter};
718 ///
719 /// fn copy<R, W>(src: &mut ZipArchive<R>, dst: &mut ZipWriter<W>) -> zip::result::ZipResult<()>
720 /// where
721 /// R: Read + Seek,
722 /// W: Write + Seek,
723 /// {
724 /// // Retrieve file entry by name
725 /// let file = src.by_name("src_file.txt")?;
726 ///
727 /// // Copy the previously obtained file entry to the destination zip archive
728 /// dst.raw_copy_file(file)?;
729 ///
730 /// Ok(())
731 /// }
732 /// ```
733 pub fn raw_copy_file(&mut self, file: ZipFile) -> ZipResult<()> {
734 let name = file.name().to_owned();
735 self.raw_copy_file_rename(file, name)
736 }
737
738 /// Add a directory entry.
739 ///
740 /// As directories have no content, you must not call [`ZipWriter::write`] before adding a new file.
741 pub fn add_directory<S>(&mut self, name: S, mut options: FileOptions) -> ZipResult<()>
742 where
743 S: Into<String>,
744 {
745 if options.permissions.is_none() {
746 options.permissions = Some(0o755);
747 }
748 *options.permissions.as_mut().unwrap() |= 0o40000;
749 options.compression_method = CompressionMethod::Stored;
750
751 let name_as_string = name.into();
752 // Append a slash to the filename if it does not end with it.
753 let name_with_slash = match name_as_string.chars().last() {
754 Some('/') | Some('\\') => name_as_string,
755 _ => name_as_string + "/",
756 };
757
758 self.start_entry(name_with_slash, options, None)?;
759 self.writing_to_file = false;
760 Ok(())
761 }
762
763 /// Add a directory entry, taking a Path as argument.
764 ///
765 /// This function ensures that the '/' path separator is used. It also ignores all non 'Normal'
766 /// Components, such as a starting '/' or '..' and '.'.
767 #[deprecated(
768 since = "0.5.7",
769 note = "by stripping `..`s from the path, the meaning of paths can change. Use `add_directory` instead."
770 )]
771 pub fn add_directory_from_path(
772 &mut self,
773 path: &std::path::Path,
774 options: FileOptions,
775 ) -> ZipResult<()> {
776 self.add_directory(path_to_string(path), options)
777 }
778
779 /// Finish the last file and write all other zip-structures
780 ///
781 /// This will return the writer, but one should normally not append any data to the end of the file.
782 /// Note that the zipfile will also be finished on drop.
783 pub fn finish(&mut self) -> ZipResult<W> {
784 self.finalize()?;
785 let inner = mem::replace(&mut self.inner, GenericZipWriter::Closed);
786 Ok(inner.unwrap())
787 }
788
789 /// Add a symlink entry.
790 ///
791 /// The zip archive will contain an entry for path `name` which is a symlink to `target`.
792 ///
793 /// No validation or normalization of the paths is performed. For best results,
794 /// callers should normalize `\` to `/` and ensure symlinks are relative to other
795 /// paths within the zip archive.
796 ///
797 /// WARNING: not all zip implementations preserve symlinks on extract. Some zip
798 /// implementations may materialize a symlink as a regular file, possibly with the
799 /// content incorrectly set to the symlink target. For maximum portability, consider
800 /// storing a regular file instead.
801 pub fn add_symlink<N, T>(
802 &mut self,
803 name: N,
804 target: T,
805 mut options: FileOptions,
806 ) -> ZipResult<()>
807 where
808 N: Into<String>,
809 T: Into<String>,
810 {
811 if options.permissions.is_none() {
812 options.permissions = Some(0o777);
813 }
814 *options.permissions.as_mut().unwrap() |= 0o120000;
815 // The symlink target is stored as file content. And compressing the target path
816 // likely wastes space. So always store.
817 options.compression_method = CompressionMethod::Stored;
818
819 self.start_entry(name, options, None)?;
820 self.writing_to_file = true;
821 self.write_all(target.into().as_bytes())?;
822 self.writing_to_file = false;
823
824 Ok(())
825 }
826
827 fn finalize(&mut self) -> ZipResult<()> {
828 self.finish_file()?;
829
830 {
831 let writer = self.inner.get_plain();
832
833 let central_start = writer.stream_position()?;
834 for file in self.files.iter() {
835 write_central_directory_header(writer, file)?;
836 }
837 let central_size = writer.stream_position()? - central_start;
838
839 if self.files.len() > spec::ZIP64_ENTRY_THR
840 || central_size.max(central_start) > spec::ZIP64_BYTES_THR
841 {
842 let zip64_footer = spec::Zip64CentralDirectoryEnd {
843 version_made_by: DEFAULT_VERSION as u16,
844 version_needed_to_extract: DEFAULT_VERSION as u16,
845 disk_number: 0,
846 disk_with_central_directory: 0,
847 number_of_files_on_this_disk: self.files.len() as u64,
848 number_of_files: self.files.len() as u64,
849 central_directory_size: central_size,
850 central_directory_offset: central_start,
851 };
852
853 zip64_footer.write(writer)?;
854
855 let zip64_footer = spec::Zip64CentralDirectoryEndLocator {
856 disk_with_central_directory: 0,
857 end_of_central_directory_offset: central_start + central_size,
858 number_of_disks: 1,
859 };
860
861 zip64_footer.write(writer)?;
862 }
863
864 let number_of_files = self.files.len().min(spec::ZIP64_ENTRY_THR) as u16;
865 let footer = spec::CentralDirectoryEnd {
866 disk_number: 0,
867 disk_with_central_directory: 0,
868 zip_file_comment: self.comment.clone(),
869 number_of_files_on_this_disk: number_of_files,
870 number_of_files,
871 central_directory_size: central_size.min(spec::ZIP64_BYTES_THR) as u32,
872 central_directory_offset: central_start.min(spec::ZIP64_BYTES_THR) as u32,
873 };
874
875 footer.write(writer)?;
876 }
877
878 Ok(())
879 }
880}
881
882impl<W: Write + io::Seek> Drop for ZipWriter<W> {
883 fn drop(&mut self) {
884 if !self.inner.is_closed() {
885 if let Err(e: ZipError) = self.finalize() {
886 let _ = write!(io::stderr(), "ZipWriter drop failed: {e:?}");
887 }
888 }
889 }
890}
891
892impl<W: Write + io::Seek> GenericZipWriter<W> {
893 fn switch_to(
894 &mut self,
895 compression: CompressionMethod,
896 compression_level: Option<i32>,
897 ) -> ZipResult<()> {
898 match self.current_compression() {
899 Some(method) if method == compression => return Ok(()),
900 None => {
901 return Err(io::Error::new(
902 io::ErrorKind::BrokenPipe,
903 "ZipWriter was already closed",
904 )
905 .into())
906 }
907 _ => {}
908 }
909
910 let bare = match mem::replace(self, GenericZipWriter::Closed) {
911 GenericZipWriter::Storer(w) => w,
912 #[cfg(any(
913 feature = "deflate",
914 feature = "deflate-miniz",
915 feature = "deflate-zlib"
916 ))]
917 GenericZipWriter::Deflater(w) => w.finish()?,
918 #[cfg(feature = "bzip2")]
919 GenericZipWriter::Bzip2(w) => w.finish()?,
920 #[cfg(feature = "zstd")]
921 GenericZipWriter::Zstd(w) => w.finish()?,
922 GenericZipWriter::Closed => {
923 return Err(io::Error::new(
924 io::ErrorKind::BrokenPipe,
925 "ZipWriter was already closed",
926 )
927 .into())
928 }
929 };
930
931 *self = {
932 #[allow(deprecated)]
933 match compression {
934 CompressionMethod::Stored => {
935 if compression_level.is_some() {
936 return Err(ZipError::UnsupportedArchive(
937 "Unsupported compression level",
938 ));
939 }
940
941 GenericZipWriter::Storer(bare)
942 }
943 #[cfg(any(
944 feature = "deflate",
945 feature = "deflate-miniz",
946 feature = "deflate-zlib"
947 ))]
948 CompressionMethod::Deflated => GenericZipWriter::Deflater(DeflateEncoder::new(
949 bare,
950 flate2::Compression::new(
951 clamp_opt(
952 compression_level
953 .unwrap_or(flate2::Compression::default().level() as i32),
954 deflate_compression_level_range(),
955 )
956 .ok_or(ZipError::UnsupportedArchive(
957 "Unsupported compression level",
958 ))? as u32,
959 ),
960 )),
961 #[cfg(feature = "bzip2")]
962 CompressionMethod::Bzip2 => GenericZipWriter::Bzip2(BzEncoder::new(
963 bare,
964 bzip2::Compression::new(
965 clamp_opt(
966 compression_level
967 .unwrap_or(bzip2::Compression::default().level() as i32),
968 bzip2_compression_level_range(),
969 )
970 .ok_or(ZipError::UnsupportedArchive(
971 "Unsupported compression level",
972 ))? as u32,
973 ),
974 )),
975 CompressionMethod::AES => {
976 return Err(ZipError::UnsupportedArchive(
977 "AES compression is not supported for writing",
978 ))
979 }
980 #[cfg(feature = "zstd")]
981 CompressionMethod::Zstd => GenericZipWriter::Zstd(
982 ZstdEncoder::new(
983 bare,
984 clamp_opt(
985 compression_level.unwrap_or(zstd::DEFAULT_COMPRESSION_LEVEL),
986 zstd::compression_level_range(),
987 )
988 .ok_or(ZipError::UnsupportedArchive(
989 "Unsupported compression level",
990 ))?,
991 )
992 .unwrap(),
993 ),
994 CompressionMethod::Unsupported(..) => {
995 return Err(ZipError::UnsupportedArchive("Unsupported compression"))
996 }
997 }
998 };
999
1000 Ok(())
1001 }
1002
1003 fn ref_mut(&mut self) -> Option<&mut dyn Write> {
1004 match *self {
1005 GenericZipWriter::Storer(ref mut w) => Some(w as &mut dyn Write),
1006 #[cfg(any(
1007 feature = "deflate",
1008 feature = "deflate-miniz",
1009 feature = "deflate-zlib"
1010 ))]
1011 GenericZipWriter::Deflater(ref mut w) => Some(w as &mut dyn Write),
1012 #[cfg(feature = "bzip2")]
1013 GenericZipWriter::Bzip2(ref mut w) => Some(w as &mut dyn Write),
1014 #[cfg(feature = "zstd")]
1015 GenericZipWriter::Zstd(ref mut w) => Some(w as &mut dyn Write),
1016 GenericZipWriter::Closed => None,
1017 }
1018 }
1019
1020 fn is_closed(&self) -> bool {
1021 matches!(*self, GenericZipWriter::Closed)
1022 }
1023
1024 fn get_plain(&mut self) -> &mut W {
1025 match *self {
1026 GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(ref mut w)) => w,
1027 _ => panic!("Should have switched to stored and unencrypted beforehand"),
1028 }
1029 }
1030
1031 fn current_compression(&self) -> Option<CompressionMethod> {
1032 match *self {
1033 GenericZipWriter::Storer(..) => Some(CompressionMethod::Stored),
1034 #[cfg(any(
1035 feature = "deflate",
1036 feature = "deflate-miniz",
1037 feature = "deflate-zlib"
1038 ))]
1039 GenericZipWriter::Deflater(..) => Some(CompressionMethod::Deflated),
1040 #[cfg(feature = "bzip2")]
1041 GenericZipWriter::Bzip2(..) => Some(CompressionMethod::Bzip2),
1042 #[cfg(feature = "zstd")]
1043 GenericZipWriter::Zstd(..) => Some(CompressionMethod::Zstd),
1044 GenericZipWriter::Closed => None,
1045 }
1046 }
1047
1048 fn unwrap(self) -> W {
1049 match self {
1050 GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(w)) => w,
1051 _ => panic!("Should have switched to stored and unencrypted beforehand"),
1052 }
1053 }
1054}
1055
1056#[cfg(any(
1057 feature = "deflate",
1058 feature = "deflate-miniz",
1059 feature = "deflate-zlib"
1060))]
1061fn deflate_compression_level_range() -> std::ops::RangeInclusive<i32> {
1062 let min: i32 = flate2::Compression::none().level() as i32;
1063 let max: i32 = flate2::Compression::best().level() as i32;
1064 min..=max
1065}
1066
1067#[cfg(feature = "bzip2")]
1068fn bzip2_compression_level_range() -> std::ops::RangeInclusive<i32> {
1069 let min = bzip2::Compression::none().level() as i32;
1070 let max = bzip2::Compression::best().level() as i32;
1071 min..=max
1072}
1073
1074#[cfg(any(
1075 feature = "deflate",
1076 feature = "deflate-miniz",
1077 feature = "deflate-zlib",
1078 feature = "bzip2",
1079 feature = "zstd"
1080))]
1081fn clamp_opt<T: Ord + Copy>(value: T, range: std::ops::RangeInclusive<T>) -> Option<T> {
1082 if range.contains(&value) {
1083 Some(value)
1084 } else {
1085 None
1086 }
1087}
1088
1089fn write_local_file_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
1090 // local file header signature
1091 writer.write_u32::<LittleEndian>(spec::LOCAL_FILE_HEADER_SIGNATURE)?;
1092 // version needed to extract
1093 writer.write_u16::<LittleEndian>(file.version_needed())?;
1094 // general purpose bit flag
1095 let flag = if !file.file_name.is_ascii() {
1096 1u16 << 11
1097 } else {
1098 0
1099 } | if file.encrypted { 1u16 << 0 } else { 0 };
1100 writer.write_u16::<LittleEndian>(flag)?;
1101 // Compression method
1102 #[allow(deprecated)]
1103 writer.write_u16::<LittleEndian>(file.compression_method.to_u16())?;
1104 // last mod file time and last mod file date
1105 writer.write_u16::<LittleEndian>(file.last_modified_time.timepart())?;
1106 writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?;
1107 // crc-32
1108 writer.write_u32::<LittleEndian>(file.crc32)?;
1109 // compressed size and uncompressed size
1110 if file.large_file {
1111 writer.write_u32::<LittleEndian>(spec::ZIP64_BYTES_THR as u32)?;
1112 writer.write_u32::<LittleEndian>(spec::ZIP64_BYTES_THR as u32)?;
1113 } else {
1114 writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
1115 writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
1116 }
1117 // file name length
1118 writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
1119 // extra field length
1120 let extra_field_length = if file.large_file { 20 } else { 0 } + file.extra_field.len() as u16;
1121 writer.write_u16::<LittleEndian>(extra_field_length)?;
1122 // file name
1123 writer.write_all(file.file_name.as_bytes())?;
1124 // zip64 extra field
1125 if file.large_file {
1126 write_local_zip64_extra_field(writer, file)?;
1127 }
1128
1129 Ok(())
1130}
1131
1132fn update_local_file_header<T: Write + io::Seek>(
1133 writer: &mut T,
1134 file: &ZipFileData,
1135) -> ZipResult<()> {
1136 const CRC32_OFFSET: u64 = 14;
1137 writer.seek(pos:io::SeekFrom::Start(file.header_start + CRC32_OFFSET))?;
1138 writer.write_u32::<LittleEndian>(file.crc32)?;
1139 if file.large_file {
1140 update_local_zip64_extra_field(writer, file)?;
1141 } else {
1142 // check compressed size as well as it can also be slightly larger than uncompressed size
1143 if file.compressed_size > spec::ZIP64_BYTES_THR {
1144 return Err(ZipError::Io(io::Error::new(
1145 kind:io::ErrorKind::Other,
1146 error:"Large file option has not been set",
1147 )));
1148 }
1149 writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
1150 // uncompressed size is already checked on write to catch it as soon as possible
1151 writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
1152 }
1153 Ok(())
1154}
1155
1156fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
1157 // buffer zip64 extra field to determine its variable length
1158 let mut zip64_extra_field = [0; 28];
1159 let zip64_extra_field_length =
1160 write_central_zip64_extra_field(&mut zip64_extra_field.as_mut(), file)?;
1161
1162 // central file header signature
1163 writer.write_u32::<LittleEndian>(spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE)?;
1164 // version made by
1165 let version_made_by = (file.system as u16) << 8 | (file.version_made_by as u16);
1166 writer.write_u16::<LittleEndian>(version_made_by)?;
1167 // version needed to extract
1168 writer.write_u16::<LittleEndian>(file.version_needed())?;
1169 // general puprose bit flag
1170 let flag = if !file.file_name.is_ascii() {
1171 1u16 << 11
1172 } else {
1173 0
1174 } | if file.encrypted { 1u16 << 0 } else { 0 };
1175 writer.write_u16::<LittleEndian>(flag)?;
1176 // compression method
1177 #[allow(deprecated)]
1178 writer.write_u16::<LittleEndian>(file.compression_method.to_u16())?;
1179 // last mod file time + date
1180 writer.write_u16::<LittleEndian>(file.last_modified_time.timepart())?;
1181 writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?;
1182 // crc-32
1183 writer.write_u32::<LittleEndian>(file.crc32)?;
1184 // compressed size
1185 writer.write_u32::<LittleEndian>(file.compressed_size.min(spec::ZIP64_BYTES_THR) as u32)?;
1186 // uncompressed size
1187 writer.write_u32::<LittleEndian>(file.uncompressed_size.min(spec::ZIP64_BYTES_THR) as u32)?;
1188 // file name length
1189 writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
1190 // extra field length
1191 writer.write_u16::<LittleEndian>(zip64_extra_field_length + file.extra_field.len() as u16)?;
1192 // file comment length
1193 writer.write_u16::<LittleEndian>(0)?;
1194 // disk number start
1195 writer.write_u16::<LittleEndian>(0)?;
1196 // internal file attribytes
1197 writer.write_u16::<LittleEndian>(0)?;
1198 // external file attributes
1199 writer.write_u32::<LittleEndian>(file.external_attributes)?;
1200 // relative offset of local header
1201 writer.write_u32::<LittleEndian>(file.header_start.min(spec::ZIP64_BYTES_THR) as u32)?;
1202 // file name
1203 writer.write_all(file.file_name.as_bytes())?;
1204 // zip64 extra field
1205 writer.write_all(&zip64_extra_field[..zip64_extra_field_length as usize])?;
1206 // extra field
1207 writer.write_all(&file.extra_field)?;
1208 // file comment
1209 // <none>
1210
1211 Ok(())
1212}
1213
1214fn validate_extra_data(file: &ZipFileData) -> ZipResult<()> {
1215 let mut data = file.extra_field.as_slice();
1216
1217 if data.len() > spec::ZIP64_ENTRY_THR {
1218 return Err(ZipError::Io(io::Error::new(
1219 io::ErrorKind::InvalidData,
1220 "Extra data exceeds extra field",
1221 )));
1222 }
1223
1224 while !data.is_empty() {
1225 let left = data.len();
1226 if left < 4 {
1227 return Err(ZipError::Io(io::Error::new(
1228 io::ErrorKind::Other,
1229 "Incomplete extra data header",
1230 )));
1231 }
1232 let kind = data.read_u16::<LittleEndian>()?;
1233 let size = data.read_u16::<LittleEndian>()? as usize;
1234 let left = left - 4;
1235
1236 if kind == 0x0001 {
1237 return Err(ZipError::Io(io::Error::new(
1238 io::ErrorKind::Other,
1239 "No custom ZIP64 extra data allowed",
1240 )));
1241 }
1242
1243 #[cfg(not(feature = "unreserved"))]
1244 {
1245 if kind <= 31 || EXTRA_FIELD_MAPPING.iter().any(|&mapped| mapped == kind) {
1246 return Err(ZipError::Io(io::Error::new(
1247 io::ErrorKind::Other,
1248 format!(
1249 "Extra data header ID {kind:#06} requires crate feature \"unreserved\"",
1250 ),
1251 )));
1252 }
1253 }
1254
1255 if size > left {
1256 return Err(ZipError::Io(io::Error::new(
1257 io::ErrorKind::Other,
1258 "Extra data size exceeds extra field",
1259 )));
1260 }
1261
1262 data = &data[size..];
1263 }
1264
1265 Ok(())
1266}
1267
1268fn write_local_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
1269 // This entry in the Local header MUST include BOTH original
1270 // and compressed file size fields.
1271 writer.write_u16::<LittleEndian>(0x0001)?;
1272 writer.write_u16::<LittleEndian>(16)?;
1273 writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
1274 writer.write_u64::<LittleEndian>(file.compressed_size)?;
1275 // Excluded fields:
1276 // u32: disk start number
1277 Ok(())
1278}
1279
1280fn update_local_zip64_extra_field<T: Write + io::Seek>(
1281 writer: &mut T,
1282 file: &ZipFileData,
1283) -> ZipResult<()> {
1284 let zip64_extra_field: u64 = file.header_start + 30 + file.file_name.as_bytes().len() as u64;
1285 writer.seek(pos:io::SeekFrom::Start(zip64_extra_field + 4))?;
1286 writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
1287 writer.write_u64::<LittleEndian>(file.compressed_size)?;
1288 // Excluded fields:
1289 // u32: disk start number
1290 Ok(())
1291}
1292
1293fn write_central_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<u16> {
1294 // The order of the fields in the zip64 extended
1295 // information record is fixed, but the fields MUST
1296 // only appear if the corresponding Local or Central
1297 // directory record field is set to 0xFFFF or 0xFFFFFFFF.
1298 let mut size = 0;
1299 let uncompressed_size = file.uncompressed_size > spec::ZIP64_BYTES_THR;
1300 let compressed_size = file.compressed_size > spec::ZIP64_BYTES_THR;
1301 let header_start = file.header_start > spec::ZIP64_BYTES_THR;
1302 if uncompressed_size {
1303 size += 8;
1304 }
1305 if compressed_size {
1306 size += 8;
1307 }
1308 if header_start {
1309 size += 8;
1310 }
1311 if size > 0 {
1312 writer.write_u16::<LittleEndian>(0x0001)?;
1313 writer.write_u16::<LittleEndian>(size)?;
1314 size += 4;
1315
1316 if uncompressed_size {
1317 writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
1318 }
1319 if compressed_size {
1320 writer.write_u64::<LittleEndian>(file.compressed_size)?;
1321 }
1322 if header_start {
1323 writer.write_u64::<LittleEndian>(file.header_start)?;
1324 }
1325 // Excluded fields:
1326 // u32: disk start number
1327 }
1328 Ok(size)
1329}
1330
1331fn path_to_string(path: &std::path::Path) -> String {
1332 let mut path_str: String = String::new();
1333 for component: Component<'_> in path.components() {
1334 if let std::path::Component::Normal(os_str: &OsStr) = component {
1335 if !path_str.is_empty() {
1336 path_str.push(ch:'/');
1337 }
1338 path_str.push_str(&os_str.to_string_lossy());
1339 }
1340 }
1341 path_str
1342}
1343
1344#[cfg(test)]
1345mod test {
1346 use super::{FileOptions, ZipWriter};
1347 use crate::compression::CompressionMethod;
1348 use crate::types::DateTime;
1349 use std::io;
1350 use std::io::Write;
1351
1352 #[test]
1353 fn write_empty_zip() {
1354 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1355 writer.set_comment("ZIP");
1356 let result = writer.finish().unwrap();
1357 assert_eq!(result.get_ref().len(), 25);
1358 assert_eq!(
1359 *result.get_ref(),
1360 [80, 75, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 90, 73, 80]
1361 );
1362 }
1363
1364 #[test]
1365 fn unix_permissions_bitmask() {
1366 // unix_permissions() throws away upper bits.
1367 let options = FileOptions::default().unix_permissions(0o120777);
1368 assert_eq!(options.permissions, Some(0o777));
1369 }
1370
1371 #[test]
1372 fn write_zip_dir() {
1373 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1374 writer
1375 .add_directory(
1376 "test",
1377 FileOptions::default().last_modified_time(
1378 DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
1379 ),
1380 )
1381 .unwrap();
1382 assert!(writer
1383 .write(b"writing to a directory is not allowed, and will not write any data")
1384 .is_err());
1385 let result = writer.finish().unwrap();
1386 assert_eq!(result.get_ref().len(), 108);
1387 assert_eq!(
1388 *result.get_ref(),
1389 &[
1390 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1391 0, 0, 5, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 1, 2, 46, 3, 20, 0, 0, 0, 0, 0,
1392 163, 165, 15, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1393 0, 0, 237, 65, 0, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0,
1394 1, 0, 51, 0, 0, 0, 35, 0, 0, 0, 0, 0,
1395 ] as &[u8]
1396 );
1397 }
1398
1399 #[test]
1400 fn write_symlink_simple() {
1401 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1402 writer
1403 .add_symlink(
1404 "name",
1405 "target",
1406 FileOptions::default().last_modified_time(
1407 DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
1408 ),
1409 )
1410 .unwrap();
1411 assert!(writer
1412 .write(b"writing to a symlink is not allowed and will not write any data")
1413 .is_err());
1414 let result = writer.finish().unwrap();
1415 assert_eq!(result.get_ref().len(), 112);
1416 assert_eq!(
1417 *result.get_ref(),
1418 &[
1419 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 252, 47, 111, 70, 6, 0, 0, 0,
1420 6, 0, 0, 0, 4, 0, 0, 0, 110, 97, 109, 101, 116, 97, 114, 103, 101, 116, 80, 75, 1,
1421 2, 46, 3, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 252, 47, 111, 70, 6, 0, 0, 0, 6, 0,
1422 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 161, 0, 0, 0, 0, 110, 97, 109, 101,
1423 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 50, 0, 0, 0, 40, 0, 0, 0, 0, 0
1424 ] as &[u8],
1425 );
1426 }
1427
1428 #[test]
1429 fn write_symlink_wonky_paths() {
1430 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1431 writer
1432 .add_symlink(
1433 "directory\\link",
1434 "/absolute/symlink\\with\\mixed/slashes",
1435 FileOptions::default().last_modified_time(
1436 DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
1437 ),
1438 )
1439 .unwrap();
1440 assert!(writer
1441 .write(b"writing to a symlink is not allowed and will not write any data")
1442 .is_err());
1443 let result = writer.finish().unwrap();
1444 assert_eq!(result.get_ref().len(), 162);
1445 assert_eq!(
1446 *result.get_ref(),
1447 &[
1448 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 95, 41, 81, 245, 36, 0, 0, 0,
1449 36, 0, 0, 0, 14, 0, 0, 0, 100, 105, 114, 101, 99, 116, 111, 114, 121, 92, 108, 105,
1450 110, 107, 47, 97, 98, 115, 111, 108, 117, 116, 101, 47, 115, 121, 109, 108, 105,
1451 110, 107, 92, 119, 105, 116, 104, 92, 109, 105, 120, 101, 100, 47, 115, 108, 97,
1452 115, 104, 101, 115, 80, 75, 1, 2, 46, 3, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 95,
1453 41, 81, 245, 36, 0, 0, 0, 36, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255,
1454 161, 0, 0, 0, 0, 100, 105, 114, 101, 99, 116, 111, 114, 121, 92, 108, 105, 110,
1455 107, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 60, 0, 0, 0, 80, 0, 0, 0, 0, 0
1456 ] as &[u8],
1457 );
1458 }
1459
1460 #[test]
1461 fn write_mimetype_zip() {
1462 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1463 let options = FileOptions {
1464 compression_method: CompressionMethod::Stored,
1465 compression_level: None,
1466 last_modified_time: DateTime::default(),
1467 permissions: Some(33188),
1468 large_file: false,
1469 encrypt_with: None,
1470 };
1471 writer.start_file("mimetype", options).unwrap();
1472 writer
1473 .write_all(b"application/vnd.oasis.opendocument.text")
1474 .unwrap();
1475 let result = writer.finish().unwrap();
1476
1477 assert_eq!(result.get_ref().len(), 153);
1478 let mut v = Vec::new();
1479 v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip"));
1480 assert_eq!(result.get_ref(), &v);
1481 }
1482
1483 #[test]
1484 fn path_to_string() {
1485 let mut path = std::path::PathBuf::new();
1486 #[cfg(windows)]
1487 path.push(r"C:\");
1488 #[cfg(unix)]
1489 path.push("/");
1490 path.push("windows");
1491 path.push("..");
1492 path.push(".");
1493 path.push("system32");
1494 let path_str = super::path_to_string(&path);
1495 assert_eq!(path_str, "windows/system32");
1496 }
1497}
1498
1499#[cfg(not(feature = "unreserved"))]
1500const EXTRA_FIELD_MAPPING: [u16; 49] = [
1501 0x0001, 0x0007, 0x0008, 0x0009, 0x000a, 0x000c, 0x000d, 0x000e, 0x000f, 0x0014, 0x0015, 0x0016,
1502 0x0017, 0x0018, 0x0019, 0x0020, 0x0021, 0x0022, 0x0023, 0x0065, 0x0066, 0x4690, 0x07c8, 0x2605,
1503 0x2705, 0x2805, 0x334d, 0x4341, 0x4453, 0x4704, 0x470f, 0x4b46, 0x4c41, 0x4d49, 0x4f4c, 0x5356,
1504 0x5455, 0x554e, 0x5855, 0x6375, 0x6542, 0x7075, 0x756e, 0x7855, 0xa11e, 0xa220, 0xfd4a, 0x9901,
1505 0x9902,
1506];
1507