1use 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 cfg_if::cfg_if;
10use crc32fast::Hasher;
11use std::convert::TryInto;
12use std::default::Default;
13use std::io;
14use std::io::prelude::*;
15use std::mem;
16
17#[cfg(any(
18 feature = "deflate",
19 feature = "deflate-miniz",
20 feature = "deflate-zlib"
21))]
22use flate2::write::DeflateEncoder;
23
24#[cfg(feature = "bzip2")]
25use bzip2::write::BzEncoder;
26
27#[cfg(feature = "time")]
28use time::OffsetDateTime;
29
30#[cfg(feature = "zstd")]
31use zstd::stream::write::Encoder as ZstdEncoder;
32
33enum MaybeEncrypted<W> {
34 Unencrypted(W),
35 Encrypted(crate::zipcrypto::ZipCryptoWriter<W>),
36}
37impl<W: Write> Write for MaybeEncrypted<W> {
38 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
39 match self {
40 MaybeEncrypted::Unencrypted(w) => w.write(buf),
41 MaybeEncrypted::Encrypted(w) => w.write(buf),
42 }
43 }
44 fn flush(&mut self) -> io::Result<()> {
45 match self {
46 MaybeEncrypted::Unencrypted(w) => w.flush(),
47 MaybeEncrypted::Encrypted(w) => w.flush(),
48 }
49 }
50}
51enum GenericZipWriter<W: Write + io::Seek> {
52 Closed,
53 Storer(MaybeEncrypted<W>),
54 #[cfg(any(
55 feature = "deflate",
56 feature = "deflate-miniz",
57 feature = "deflate-zlib"
58 ))]
59 Deflater(DeflateEncoder<MaybeEncrypted<W>>),
60 #[cfg(feature = "bzip2")]
61 Bzip2(BzEncoder<MaybeEncrypted<W>>),
62 #[cfg(feature = "zstd")]
63 Zstd(ZstdEncoder<'static, MaybeEncrypted<W>>),
64}
65pub(crate) mod zip_writer {
67 use super::*;
68 pub struct ZipWriter<W: Write + io::Seek> {
97 pub(super) inner: GenericZipWriter<W>,
98 pub(super) files: Vec<ZipFileData>,
99 pub(super) stats: ZipWriterStats,
100 pub(super) writing_to_file: bool,
101 pub(super) writing_to_extra_field: bool,
102 pub(super) writing_to_central_extra_field_only: bool,
103 pub(super) writing_raw: bool,
104 pub(super) comment: Vec<u8>,
105 }
106}
107pub use zip_writer::ZipWriter;
108
109#[derive(Default)]
110struct ZipWriterStats {
111 hasher: Hasher,
112 start: u64,
113 bytes_written: u64,
114}
115
116struct ZipRawValues {
117 crc32: u32,
118 compressed_size: u64,
119 uncompressed_size: u64,
120}
121
122#[derive(Copy, Clone)]
124pub struct FileOptions {
125 compression_method: CompressionMethod,
126 compression_level: Option<i32>,
127 last_modified_time: DateTime,
128 permissions: Option<u32>,
129 large_file: bool,
130 encrypt_with: Option<crate::zipcrypto::ZipCryptoKeys>,
131}
132
133impl FileOptions {
134 #[must_use]
139 pub fn compression_method(mut self, method: CompressionMethod) -> FileOptions {
140 self.compression_method = method;
141 self
142 }
143
144 #[must_use]
154 pub fn compression_level(mut self, level: Option<i32>) -> FileOptions {
155 self.compression_level = level;
156 self
157 }
158
159 #[must_use]
164 pub fn last_modified_time(mut self, mod_time: DateTime) -> FileOptions {
165 self.last_modified_time = mod_time;
166 self
167 }
168
169 #[must_use]
179 pub fn unix_permissions(mut self, mode: u32) -> FileOptions {
180 self.permissions = Some(mode & 0o777);
181 self
182 }
183
184 #[must_use]
190 pub fn large_file(mut self, large: bool) -> FileOptions {
191 self.large_file = large;
192 self
193 }
194 pub(crate) fn with_deprecated_encryption(mut self, password: &[u8]) -> FileOptions {
195 self.encrypt_with = Some(crate::zipcrypto::ZipCryptoKeys::derive(password));
196 self
197 }
198}
199
200impl Default for FileOptions {
201 fn default() -> Self {
203 cfg_if! {
204 if #[cfg(any(
205 feature = "deflate",
206 feature = "deflate-miniz",
207 feature = "deflate-zlib"
208 ))] {
209 let compression_method = CompressionMethod::Deflated;
210 } else {
211 let compression_method = CompressionMethod::Stored;
212 }
213 }
214 cfg_if! {
215 if #[cfg(feature = "time")] {
216 let last_modified_time = OffsetDateTime::now_utc().try_into().unwrap_or_default();
217 } else {
218 let last_modified_time = DateTime::default();
219 }
220 }
221 Self {
222 compression_method,
223 compression_level: None,
224 last_modified_time,
225 permissions: None,
226 large_file: false,
227 encrypt_with: None,
228 }
229 }
230}
231
232impl<W: Write + io::Seek> Write for ZipWriter<W> {
233 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
234 if !self.writing_to_file {
235 return Err(io::Error::new(
236 io::ErrorKind::Other,
237 "No file has been started",
238 ));
239 }
240 match self.inner.ref_mut() {
241 Some(ref mut w) => {
242 if self.writing_to_extra_field {
243 self.files.last_mut().unwrap().extra_field.write(buf)
244 } else {
245 let write_result = w.write(buf);
246 if let Ok(count) = write_result {
247 self.stats.update(&buf[0..count]);
248 if self.stats.bytes_written > spec::ZIP64_BYTES_THR
249 && !self.files.last_mut().unwrap().large_file
250 {
251 let _inner = mem::replace(&mut self.inner, GenericZipWriter::Closed);
252 return Err(io::Error::new(
253 io::ErrorKind::Other,
254 "Large file option has not been set",
255 ));
256 }
257 }
258 write_result
259 }
260 }
261 None => Err(io::Error::new(
262 io::ErrorKind::BrokenPipe,
263 "ZipWriter was already closed",
264 )),
265 }
266 }
267
268 fn flush(&mut self) -> io::Result<()> {
269 match self.inner.ref_mut() {
270 Some(ref mut w) => w.flush(),
271 None => Err(io::Error::new(
272 io::ErrorKind::BrokenPipe,
273 "ZipWriter was already closed",
274 )),
275 }
276 }
277}
278
279impl ZipWriterStats {
280 fn update(&mut self, buf: &[u8]) {
281 self.hasher.update(buf);
282 self.bytes_written += buf.len() as u64;
283 }
284}
285
286impl<A: Read + Write + io::Seek> ZipWriter<A> {
287 pub fn new_append(mut readwriter: A) -> ZipResult<ZipWriter<A>> {
289 let (footer, cde_end_pos) = spec::CentralDirectoryEnd::find_and_parse(&mut readwriter)?;
290
291 if footer.disk_number != footer.disk_with_central_directory {
292 return Err(ZipError::UnsupportedArchive(
293 "Support for multi-disk files is not implemented",
294 ));
295 }
296
297 let (archive_offset, directory_start, number_of_files) =
298 ZipArchive::get_directory_counts(&mut readwriter, &footer, cde_end_pos)?;
299
300 if readwriter
301 .seek(io::SeekFrom::Start(directory_start))
302 .is_err()
303 {
304 return Err(ZipError::InvalidArchive(
305 "Could not seek to start of central directory",
306 ));
307 }
308
309 let files = (0..number_of_files)
310 .map(|_| central_header_to_zip_file(&mut readwriter, archive_offset))
311 .collect::<Result<Vec<_>, _>>()?;
312
313 let _ = readwriter.seek(io::SeekFrom::Start(directory_start)); Ok(ZipWriter {
316 inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(readwriter)),
317 files,
318 stats: Default::default(),
319 writing_to_file: false,
320 writing_to_extra_field: false,
321 writing_to_central_extra_field_only: false,
322 comment: footer.zip_file_comment,
323 writing_raw: true, })
325 }
326
327 #[cfg(feature = "merge")]
333 #[cfg_attr(docsrs, doc(cfg(feature = "merge")))]
334 pub fn finish_into_readable(&mut self) -> ZipResult<ZipArchive<A>> {
335 let cde_start = self.finalize()?;
336 let inner = mem::replace(&mut self.inner, GenericZipWriter::Closed);
337 let comment = mem::take(&mut self.comment);
338 let files = mem::take(&mut self.files);
339 let archive = ZipArchive::from_finalized_writer(cde_start, files, comment, inner.unwrap());
340 Ok(archive)
341 }
342}
343
344impl<W: Write + io::Seek> ZipWriter<W> {
345 pub fn new(inner: W) -> ZipWriter<W> {
349 ZipWriter {
350 inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(inner)),
351 files: Vec::new(),
352 stats: Default::default(),
353 writing_to_file: false,
354 writing_to_extra_field: false,
355 writing_to_central_extra_field_only: false,
356 writing_raw: false,
357 comment: Vec::new(),
358 }
359 }
360
361 pub fn set_comment<S>(&mut self, comment: S)
363 where
364 S: Into<String>,
365 {
366 self.set_raw_comment(comment.into().into())
367 }
368
369 pub fn set_raw_comment(&mut self, comment: Vec<u8>) {
374 self.comment = comment;
375 }
376
377 fn start_entry<S>(
379 &mut self,
380 name: S,
381 options: FileOptions,
382 raw_values: Option<ZipRawValues>,
383 ) -> ZipResult<()>
384 where
385 S: Into<String>,
386 {
387 self.finish_file()?;
388
389 let raw_values = raw_values.unwrap_or(ZipRawValues {
390 crc32: 0,
391 compressed_size: 0,
392 uncompressed_size: 0,
393 });
394
395 {
396 let writer = self.inner.get_plain();
397 let header_start = writer.stream_position()?;
398
399 let permissions = options.permissions.unwrap_or(0o100644);
400 let mut file = ZipFileData {
401 system: System::Unix,
402 version_made_by: DEFAULT_VERSION,
403 encrypted: options.encrypt_with.is_some(),
404 using_data_descriptor: false,
405 compression_method: options.compression_method,
406 compression_level: options.compression_level,
407 last_modified_time: options.last_modified_time,
408 crc32: raw_values.crc32,
409 compressed_size: raw_values.compressed_size,
410 uncompressed_size: raw_values.uncompressed_size,
411 file_name: name.into(),
412 file_name_raw: Vec::new(), extra_field: Vec::new(),
414 file_comment: String::new(),
415 header_start,
416 data_start: AtomicU64::new(0),
417 central_header_start: 0,
418 external_attributes: permissions << 16,
419 large_file: options.large_file,
420 aes_mode: None,
421 };
422 write_local_file_header(writer, &file)?;
423
424 let header_end = writer.stream_position()?;
425 self.stats.start = header_end;
426 *file.data_start.get_mut() = header_end;
427
428 self.stats.bytes_written = 0;
429 self.stats.hasher = Hasher::new();
430
431 self.files.push(file);
432 }
433 if let Some(keys) = options.encrypt_with {
434 let mut zipwriter = crate::zipcrypto::ZipCryptoWriter {
435 writer: core::mem::replace(&mut self.inner, GenericZipWriter::Closed).unwrap(),
436 buffer: vec![],
437 keys,
438 };
439 let crypto_header = [0u8; 12];
440
441 zipwriter.write_all(&crypto_header)?;
442 self.inner = GenericZipWriter::Storer(MaybeEncrypted::Encrypted(zipwriter));
443 }
444 Ok(())
445 }
446
447 fn finish_file(&mut self) -> ZipResult<()> {
448 if self.writing_to_extra_field {
449 self.end_extra_data()?;
451 }
452 self.inner.switch_to(CompressionMethod::Stored, None)?;
453 match core::mem::replace(&mut self.inner, GenericZipWriter::Closed) {
454 GenericZipWriter::Storer(MaybeEncrypted::Encrypted(writer)) => {
455 let crc32 = self.stats.hasher.clone().finalize();
456 self.inner =
457 GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(writer.finish(crc32)?))
458 }
459 GenericZipWriter::Storer(w) => self.inner = GenericZipWriter::Storer(w),
460 _ => unreachable!(),
461 }
462 let writer = self.inner.get_plain();
463
464 if !self.writing_raw {
465 let file = match self.files.last_mut() {
466 None => return Ok(()),
467 Some(f) => f,
468 };
469 file.crc32 = self.stats.hasher.clone().finalize();
470 file.uncompressed_size = self.stats.bytes_written;
471
472 let file_end = writer.stream_position()?;
473 file.compressed_size = file_end - self.stats.start;
474
475 update_local_file_header(writer, file)?;
476 writer.seek(io::SeekFrom::Start(file_end))?;
477 }
478
479 self.writing_to_file = false;
480 self.writing_raw = false;
481 Ok(())
482 }
483
484 #[cfg(feature = "merge")]
490 #[cfg_attr(docsrs, doc(cfg(feature = "merge")))]
491 pub fn merge_archive<R>(&mut self, mut source: ZipArchive<R>) -> ZipResult<()>
492 where
493 R: Read + io::Seek,
494 {
495 self.finish_file()?;
496
497 self.writing_to_file = true;
500 self.writing_raw = true;
501
502 let writer = self.inner.get_plain();
503 let new_files = source.merge_contents(writer)?;
505 self.files
507 .extend(new_files.into_iter().map(|(_, entry)| entry));
508
509 Ok(())
510 }
511
512 pub fn start_file<S>(&mut self, name: S, mut options: FileOptions) -> ZipResult<()>
516 where
517 S: Into<String>,
518 {
519 if options.permissions.is_none() {
520 options.permissions = Some(0o644);
521 }
522 *options.permissions.as_mut().unwrap() |= 0o100000;
523 self.start_entry(name, options, None)?;
524 self.inner
525 .switch_to(options.compression_method, options.compression_level)?;
526 self.writing_to_file = true;
527 Ok(())
528 }
529
530 #[deprecated(
535 since = "0.5.7",
536 note = "by stripping `..`s from the path, the meaning of paths can change. Use `start_file` instead."
537 )]
538 pub fn start_file_from_path(
539 &mut self,
540 path: &std::path::Path,
541 options: FileOptions,
542 ) -> ZipResult<()> {
543 self.start_file(path_to_string(path), options)
544 }
545
546 pub fn start_file_aligned<S>(
552 &mut self,
553 name: S,
554 options: FileOptions,
555 align: u16,
556 ) -> Result<u64, ZipError>
557 where
558 S: Into<String>,
559 {
560 let data_start = self.start_file_with_extra_data(name, options)?;
561 let align = align as u64;
562 if align > 1 && data_start % align != 0 {
563 let pad_length = (align - (data_start + 4) % align) % align;
564 let pad = vec![0; pad_length as usize];
565 self.write_all(b"za").map_err(ZipError::from)?; self.write_u16::<LittleEndian>(pad.len() as u16)
567 .map_err(ZipError::from)?;
568 self.write_all(&pad).map_err(ZipError::from)?;
569 assert_eq!(self.end_local_start_central_extra_data()? % align, 0);
570 }
571 let extra_data_end = self.end_extra_data()?;
572 Ok(extra_data_end - data_start)
573 }
574
575 pub fn start_file_with_extra_data<S>(
639 &mut self,
640 name: S,
641 mut options: FileOptions,
642 ) -> ZipResult<u64>
643 where
644 S: Into<String>,
645 {
646 if options.permissions.is_none() {
647 options.permissions = Some(0o644);
648 }
649 *options.permissions.as_mut().unwrap() |= 0o100000;
650 self.start_entry(name, options, None)?;
651 self.writing_to_file = true;
652 self.writing_to_extra_field = true;
653 Ok(self.files.last().unwrap().data_start.load())
654 }
655
656 pub fn end_local_start_central_extra_data(&mut self) -> ZipResult<u64> {
660 let data_start = self.end_extra_data()?;
661 self.files.last_mut().unwrap().extra_field.clear();
662 self.writing_to_extra_field = true;
663 self.writing_to_central_extra_field_only = true;
664 Ok(data_start)
665 }
666
667 pub fn end_extra_data(&mut self) -> ZipResult<u64> {
671 if !self.writing_to_extra_field {
673 return Err(ZipError::Io(io::Error::new(
674 io::ErrorKind::Other,
675 "Not writing to extra field",
676 )));
677 }
678 let file = self.files.last_mut().unwrap();
679
680 validate_extra_data(file)?;
681
682 let data_start = file.data_start.get_mut();
683
684 if !self.writing_to_central_extra_field_only {
685 let writer = self.inner.get_plain();
686
687 writer.write_all(&file.extra_field)?;
689
690 let header_end = *data_start + file.extra_field.len() as u64;
692 self.stats.start = header_end;
693 *data_start = header_end;
694
695 let extra_field_length =
697 if file.large_file { 20 } else { 0 } + file.extra_field.len() as u16;
698 writer.seek(io::SeekFrom::Start(file.header_start + 28))?;
699 writer.write_u16::<LittleEndian>(extra_field_length)?;
700 writer.seek(io::SeekFrom::Start(header_end))?;
701
702 self.inner
703 .switch_to(file.compression_method, file.compression_level)?;
704 }
705
706 self.writing_to_extra_field = false;
707 self.writing_to_central_extra_field_only = false;
708 Ok(*data_start)
709 }
710
711 pub fn raw_copy_file_rename<S>(&mut self, mut file: ZipFile, name: S) -> ZipResult<()>
738 where
739 S: Into<String>,
740 {
741 let mut options = FileOptions::default()
742 .large_file(file.compressed_size().max(file.size()) > spec::ZIP64_BYTES_THR)
743 .last_modified_time(file.last_modified())
744 .compression_method(file.compression());
745 if let Some(perms) = file.unix_mode() {
746 options = options.unix_permissions(perms);
747 }
748
749 let raw_values = ZipRawValues {
750 crc32: file.crc32(),
751 compressed_size: file.compressed_size(),
752 uncompressed_size: file.size(),
753 };
754
755 self.start_entry(name, options, Some(raw_values))?;
756 self.writing_to_file = true;
757 self.writing_raw = true;
758
759 io::copy(file.get_raw_reader(), self)?;
760
761 Ok(())
762 }
763
764 pub fn raw_copy_file(&mut self, file: ZipFile) -> ZipResult<()> {
788 let name = file.name().to_owned();
789 self.raw_copy_file_rename(file, name)
790 }
791
792 pub fn add_directory<S>(&mut self, name: S, mut options: FileOptions) -> ZipResult<()>
796 where
797 S: Into<String>,
798 {
799 if options.permissions.is_none() {
800 options.permissions = Some(0o755);
801 }
802 *options.permissions.as_mut().unwrap() |= 0o40000;
803 options.compression_method = CompressionMethod::Stored;
804
805 let name_as_string = name.into();
806 let name_with_slash = match name_as_string.chars().last() {
808 Some('/') | Some('\\') => name_as_string,
809 _ => name_as_string + "/",
810 };
811
812 self.start_entry(name_with_slash, options, None)?;
813 self.writing_to_file = false;
814 Ok(())
815 }
816
817 #[deprecated(
822 since = "0.5.7",
823 note = "by stripping `..`s from the path, the meaning of paths can change. Use `add_directory` instead."
824 )]
825 pub fn add_directory_from_path(
826 &mut self,
827 path: &std::path::Path,
828 options: FileOptions,
829 ) -> ZipResult<()> {
830 self.add_directory(path_to_string(path), options)
831 }
832
833 pub fn finish(&mut self) -> ZipResult<W> {
838 let _ = self.finalize()?;
839 let inner = mem::replace(&mut self.inner, GenericZipWriter::Closed);
840 Ok(inner.unwrap())
841 }
842
843 pub fn add_symlink<N, T>(
856 &mut self,
857 name: N,
858 target: T,
859 mut options: FileOptions,
860 ) -> ZipResult<()>
861 where
862 N: Into<String>,
863 T: Into<String>,
864 {
865 if options.permissions.is_none() {
866 options.permissions = Some(0o777);
867 }
868 *options.permissions.as_mut().unwrap() |= 0o120000;
869 options.compression_method = CompressionMethod::Stored;
872
873 self.start_entry(name, options, None)?;
874 self.writing_to_file = true;
875 self.write_all(target.into().as_bytes())?;
876 self.writing_to_file = false;
877
878 Ok(())
879 }
880
881 fn finalize(&mut self) -> ZipResult<u64> {
882 self.finish_file()?;
883
884 let central_start = {
885 let writer = self.inner.get_plain();
886
887 let central_start = writer.stream_position()?;
888 for file in self.files.iter() {
889 write_central_directory_header(writer, file)?;
890 }
891 let central_size = writer.stream_position()? - central_start;
892
893 if self.files.len() > spec::ZIP64_ENTRY_THR
894 || central_size.max(central_start) > spec::ZIP64_BYTES_THR
895 {
896 let zip64_footer = spec::Zip64CentralDirectoryEnd {
897 version_made_by: DEFAULT_VERSION as u16,
898 version_needed_to_extract: DEFAULT_VERSION as u16,
899 disk_number: 0,
900 disk_with_central_directory: 0,
901 number_of_files_on_this_disk: self.files.len() as u64,
902 number_of_files: self.files.len() as u64,
903 central_directory_size: central_size,
904 central_directory_offset: central_start,
905 };
906
907 zip64_footer.write(writer)?;
908
909 let zip64_footer = spec::Zip64CentralDirectoryEndLocator {
910 disk_with_central_directory: 0,
911 end_of_central_directory_offset: central_start + central_size,
912 number_of_disks: 1,
913 };
914
915 zip64_footer.write(writer)?;
916 }
917
918 let number_of_files = self.files.len().min(spec::ZIP64_ENTRY_THR) as u16;
919 let footer = spec::CentralDirectoryEnd {
920 disk_number: 0,
921 disk_with_central_directory: 0,
922 zip_file_comment: self.comment.clone(),
923 number_of_files_on_this_disk: number_of_files,
924 number_of_files,
925 central_directory_size: central_size.min(spec::ZIP64_BYTES_THR) as u32,
926 central_directory_offset: central_start.min(spec::ZIP64_BYTES_THR) as u32,
927 };
928
929 footer.write(writer)?;
930
931 central_start
932 };
933
934 Ok(central_start)
935 }
936}
937
938impl<W: Write + io::Seek> Drop for ZipWriter<W> {
939 fn drop(&mut self) {
940 if !self.inner.is_closed() {
941 if let Err(e) = self.finalize() {
942 let _ = write!(io::stderr(), "ZipWriter drop failed: {e:?}");
943 }
944 }
945 }
946}
947
948impl<W: Write + io::Seek> GenericZipWriter<W> {
949 fn switch_to(
950 &mut self,
951 compression: CompressionMethod,
952 compression_level: Option<i32>,
953 ) -> ZipResult<()> {
954 match self.current_compression() {
955 Some(method) if method == compression => return Ok(()),
956 None => {
957 return Err(io::Error::new(
958 io::ErrorKind::BrokenPipe,
959 "ZipWriter was already closed",
960 )
961 .into())
962 }
963 _ => {}
964 }
965
966 let bare = match mem::replace(self, GenericZipWriter::Closed) {
967 GenericZipWriter::Storer(w) => w,
968 #[cfg(any(
969 feature = "deflate",
970 feature = "deflate-miniz",
971 feature = "deflate-zlib"
972 ))]
973 GenericZipWriter::Deflater(w) => w.finish()?,
974 #[cfg(feature = "bzip2")]
975 GenericZipWriter::Bzip2(w) => w.finish()?,
976 #[cfg(feature = "zstd")]
977 GenericZipWriter::Zstd(w) => w.finish()?,
978 GenericZipWriter::Closed => {
979 return Err(io::Error::new(
980 io::ErrorKind::BrokenPipe,
981 "ZipWriter was already closed",
982 )
983 .into())
984 }
985 };
986
987 *self = {
988 #[allow(deprecated)]
989 match compression {
990 CompressionMethod::Stored => {
991 if compression_level.is_some() {
992 return Err(ZipError::UnsupportedArchive(
993 "Unsupported compression level",
994 ));
995 }
996
997 GenericZipWriter::Storer(bare)
998 }
999 #[cfg(any(
1000 feature = "deflate",
1001 feature = "deflate-miniz",
1002 feature = "deflate-zlib"
1003 ))]
1004 CompressionMethod::Deflated => GenericZipWriter::Deflater(DeflateEncoder::new(
1005 bare,
1006 flate2::Compression::new(
1007 clamp_opt(
1008 compression_level
1009 .unwrap_or(flate2::Compression::default().level() as i32),
1010 deflate_compression_level_range(),
1011 )
1012 .ok_or(ZipError::UnsupportedArchive(
1013 "Unsupported compression level",
1014 ))? as u32,
1015 ),
1016 )),
1017 #[cfg(feature = "bzip2")]
1018 CompressionMethod::Bzip2 => GenericZipWriter::Bzip2(BzEncoder::new(
1019 bare,
1020 bzip2::Compression::new(
1021 clamp_opt(
1022 compression_level
1023 .unwrap_or(bzip2::Compression::default().level() as i32),
1024 bzip2_compression_level_range(),
1025 )
1026 .ok_or(ZipError::UnsupportedArchive(
1027 "Unsupported compression level",
1028 ))? as u32,
1029 ),
1030 )),
1031 CompressionMethod::AES => {
1032 return Err(ZipError::UnsupportedArchive(
1033 "AES compression is not supported for writing",
1034 ))
1035 }
1036 #[cfg(feature = "zstd")]
1037 CompressionMethod::Zstd => GenericZipWriter::Zstd(
1038 ZstdEncoder::new(
1039 bare,
1040 clamp_opt(
1041 compression_level.unwrap_or(zstd::DEFAULT_COMPRESSION_LEVEL),
1042 zstd::compression_level_range(),
1043 )
1044 .ok_or(ZipError::UnsupportedArchive(
1045 "Unsupported compression level",
1046 ))?,
1047 )
1048 .unwrap(),
1049 ),
1050 CompressionMethod::Unsupported(..) => {
1051 return Err(ZipError::UnsupportedArchive("Unsupported compression"))
1052 }
1053 }
1054 };
1055
1056 Ok(())
1057 }
1058
1059 fn ref_mut(&mut self) -> Option<&mut dyn Write> {
1060 match *self {
1061 GenericZipWriter::Storer(ref mut w) => Some(w as &mut dyn Write),
1062 #[cfg(any(
1063 feature = "deflate",
1064 feature = "deflate-miniz",
1065 feature = "deflate-zlib"
1066 ))]
1067 GenericZipWriter::Deflater(ref mut w) => Some(w as &mut dyn Write),
1068 #[cfg(feature = "bzip2")]
1069 GenericZipWriter::Bzip2(ref mut w) => Some(w as &mut dyn Write),
1070 #[cfg(feature = "zstd")]
1071 GenericZipWriter::Zstd(ref mut w) => Some(w as &mut dyn Write),
1072 GenericZipWriter::Closed => None,
1073 }
1074 }
1075
1076 fn is_closed(&self) -> bool {
1077 matches!(*self, GenericZipWriter::Closed)
1078 }
1079
1080 fn get_plain(&mut self) -> &mut W {
1081 match *self {
1082 GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(ref mut w)) => w,
1083 _ => panic!("Should have switched to stored and unencrypted beforehand"),
1084 }
1085 }
1086
1087 fn current_compression(&self) -> Option<CompressionMethod> {
1088 match *self {
1089 GenericZipWriter::Storer(..) => Some(CompressionMethod::Stored),
1090 #[cfg(any(
1091 feature = "deflate",
1092 feature = "deflate-miniz",
1093 feature = "deflate-zlib"
1094 ))]
1095 GenericZipWriter::Deflater(..) => Some(CompressionMethod::Deflated),
1096 #[cfg(feature = "bzip2")]
1097 GenericZipWriter::Bzip2(..) => Some(CompressionMethod::Bzip2),
1098 #[cfg(feature = "zstd")]
1099 GenericZipWriter::Zstd(..) => Some(CompressionMethod::Zstd),
1100 GenericZipWriter::Closed => None,
1101 }
1102 }
1103
1104 fn unwrap(self) -> W {
1105 match self {
1106 GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(w)) => w,
1107 _ => panic!("Should have switched to stored and unencrypted beforehand"),
1108 }
1109 }
1110}
1111
1112#[cfg(any(
1113 feature = "deflate",
1114 feature = "deflate-miniz",
1115 feature = "deflate-zlib"
1116))]
1117fn deflate_compression_level_range() -> std::ops::RangeInclusive<i32> {
1118 let min = flate2::Compression::none().level() as i32;
1119 let max = flate2::Compression::best().level() as i32;
1120 min..=max
1121}
1122
1123#[cfg(feature = "bzip2")]
1124fn bzip2_compression_level_range() -> std::ops::RangeInclusive<i32> {
1125 let min = bzip2::Compression::none().level() as i32;
1126 let max = bzip2::Compression::best().level() as i32;
1127 min..=max
1128}
1129
1130#[cfg(any(
1131 feature = "deflate",
1132 feature = "deflate-miniz",
1133 feature = "deflate-zlib",
1134 feature = "bzip2",
1135 feature = "zstd"
1136))]
1137fn clamp_opt<T: Ord + Copy>(value: T, range: std::ops::RangeInclusive<T>) -> Option<T> {
1138 if range.contains(&value) {
1139 Some(value)
1140 } else {
1141 None
1142 }
1143}
1144
1145fn write_local_file_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
1146 writer.write_u32::<LittleEndian>(spec::LOCAL_FILE_HEADER_SIGNATURE)?;
1148 writer.write_u16::<LittleEndian>(file.version_needed())?;
1150 let flag = if !file.file_name.is_ascii() {
1152 1u16 << 11
1153 } else {
1154 0
1155 } | if file.encrypted { 1u16 << 0 } else { 0 };
1156 writer.write_u16::<LittleEndian>(flag)?;
1157 #[allow(deprecated)]
1159 writer.write_u16::<LittleEndian>(file.compression_method.to_u16())?;
1160 writer.write_u16::<LittleEndian>(file.last_modified_time.timepart())?;
1162 writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?;
1163 writer.write_u32::<LittleEndian>(file.crc32)?;
1165 if file.large_file {
1167 writer.write_u32::<LittleEndian>(spec::ZIP64_BYTES_THR as u32)?;
1168 writer.write_u32::<LittleEndian>(spec::ZIP64_BYTES_THR as u32)?;
1169 } else {
1170 writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
1171 writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
1172 }
1173 writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
1175 let extra_field_length = if file.large_file { 20 } else { 0 } + file.extra_field.len() as u16;
1177 writer.write_u16::<LittleEndian>(extra_field_length)?;
1178 writer.write_all(file.file_name.as_bytes())?;
1180 if file.large_file {
1182 write_local_zip64_extra_field(writer, file)?;
1183 }
1184
1185 Ok(())
1186}
1187
1188fn update_local_file_header<T: Write + io::Seek>(
1189 writer: &mut T,
1190 file: &ZipFileData,
1191) -> ZipResult<()> {
1192 const CRC32_OFFSET: u64 = 14;
1193 writer.seek(io::SeekFrom::Start(file.header_start + CRC32_OFFSET))?;
1194 writer.write_u32::<LittleEndian>(file.crc32)?;
1195 if file.large_file {
1196 update_local_zip64_extra_field(writer, file)?;
1197 } else {
1198 if file.compressed_size > spec::ZIP64_BYTES_THR {
1200 return Err(ZipError::Io(io::Error::new(
1201 io::ErrorKind::Other,
1202 "Large file option has not been set",
1203 )));
1204 }
1205 writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
1206 writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
1208 }
1209 Ok(())
1210}
1211
1212fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
1213 let mut zip64_extra_field = [0; 28];
1215 let zip64_extra_field_length =
1216 write_central_zip64_extra_field(&mut zip64_extra_field.as_mut(), file)?;
1217
1218 writer.write_u32::<LittleEndian>(spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE)?;
1220 let version_made_by = (file.system as u16) << 8 | (file.version_made_by as u16);
1222 writer.write_u16::<LittleEndian>(version_made_by)?;
1223 writer.write_u16::<LittleEndian>(file.version_needed())?;
1225 let flag = if !file.file_name.is_ascii() {
1227 1u16 << 11
1228 } else {
1229 0
1230 } | if file.encrypted { 1u16 << 0 } else { 0 };
1231 writer.write_u16::<LittleEndian>(flag)?;
1232 #[allow(deprecated)]
1234 writer.write_u16::<LittleEndian>(file.compression_method.to_u16())?;
1235 writer.write_u16::<LittleEndian>(file.last_modified_time.timepart())?;
1237 writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?;
1238 writer.write_u32::<LittleEndian>(file.crc32)?;
1240 writer.write_u32::<LittleEndian>(file.compressed_size.min(spec::ZIP64_BYTES_THR) as u32)?;
1242 writer.write_u32::<LittleEndian>(file.uncompressed_size.min(spec::ZIP64_BYTES_THR) as u32)?;
1244 writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
1246 writer.write_u16::<LittleEndian>(zip64_extra_field_length + file.extra_field.len() as u16)?;
1248 writer.write_u16::<LittleEndian>(0)?;
1250 writer.write_u16::<LittleEndian>(0)?;
1252 writer.write_u16::<LittleEndian>(0)?;
1254 writer.write_u32::<LittleEndian>(file.external_attributes)?;
1256 writer.write_u32::<LittleEndian>(file.header_start.min(spec::ZIP64_BYTES_THR) as u32)?;
1258 writer.write_all(file.file_name.as_bytes())?;
1260 writer.write_all(&zip64_extra_field[..zip64_extra_field_length as usize])?;
1262 writer.write_all(&file.extra_field)?;
1264 Ok(())
1268}
1269
1270fn validate_extra_data(file: &ZipFileData) -> ZipResult<()> {
1271 let mut data = file.extra_field.as_slice();
1272
1273 if data.len() > spec::ZIP64_ENTRY_THR {
1274 return Err(ZipError::Io(io::Error::new(
1275 io::ErrorKind::InvalidData,
1276 "Extra data exceeds extra field",
1277 )));
1278 }
1279
1280 while !data.is_empty() {
1281 let left = data.len();
1282 if left < 4 {
1283 return Err(ZipError::Io(io::Error::new(
1284 io::ErrorKind::Other,
1285 "Incomplete extra data header",
1286 )));
1287 }
1288 let kind = data.read_u16::<LittleEndian>()?;
1289 let size = data.read_u16::<LittleEndian>()? as usize;
1290 let left = left - 4;
1291
1292 if kind == 0x0001 {
1293 return Err(ZipError::Io(io::Error::new(
1294 io::ErrorKind::Other,
1295 "No custom ZIP64 extra data allowed",
1296 )));
1297 }
1298
1299 #[cfg(not(feature = "unreserved"))]
1300 {
1301 if kind <= 31 || EXTRA_FIELD_MAPPING.iter().any(|&mapped| mapped == kind) {
1302 return Err(ZipError::Io(io::Error::new(
1303 io::ErrorKind::Other,
1304 format!(
1305 "Extra data header ID {kind:#06} requires crate feature \"unreserved\"",
1306 ),
1307 )));
1308 }
1309 }
1310
1311 if size > left {
1312 return Err(ZipError::Io(io::Error::new(
1313 io::ErrorKind::Other,
1314 "Extra data size exceeds extra field",
1315 )));
1316 }
1317
1318 data = &data[size..];
1319 }
1320
1321 Ok(())
1322}
1323
1324fn write_local_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
1325 writer.write_u16::<LittleEndian>(0x0001)?;
1328 writer.write_u16::<LittleEndian>(16)?;
1329 writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
1330 writer.write_u64::<LittleEndian>(file.compressed_size)?;
1331 Ok(())
1334}
1335
1336fn update_local_zip64_extra_field<T: Write + io::Seek>(
1337 writer: &mut T,
1338 file: &ZipFileData,
1339) -> ZipResult<()> {
1340 let zip64_extra_field = file.header_start + 30 + file.file_name.as_bytes().len() as u64;
1341 writer.seek(io::SeekFrom::Start(zip64_extra_field + 4))?;
1342 writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
1343 writer.write_u64::<LittleEndian>(file.compressed_size)?;
1344 Ok(())
1347}
1348
1349fn write_central_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<u16> {
1350 let mut size = 0;
1355 let uncompressed_size = file.uncompressed_size > spec::ZIP64_BYTES_THR;
1356 let compressed_size = file.compressed_size > spec::ZIP64_BYTES_THR;
1357 let header_start = file.header_start > spec::ZIP64_BYTES_THR;
1358 if uncompressed_size {
1359 size += 8;
1360 }
1361 if compressed_size {
1362 size += 8;
1363 }
1364 if header_start {
1365 size += 8;
1366 }
1367 if size > 0 {
1368 writer.write_u16::<LittleEndian>(0x0001)?;
1369 writer.write_u16::<LittleEndian>(size)?;
1370 size += 4;
1371
1372 if uncompressed_size {
1373 writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
1374 }
1375 if compressed_size {
1376 writer.write_u64::<LittleEndian>(file.compressed_size)?;
1377 }
1378 if header_start {
1379 writer.write_u64::<LittleEndian>(file.header_start)?;
1380 }
1381 }
1384 Ok(size)
1385}
1386
1387fn path_to_string(path: &std::path::Path) -> String {
1388 let mut path_str = String::new();
1389 for component in path.components() {
1390 if let std::path::Component::Normal(os_str) = component {
1391 if !path_str.is_empty() {
1392 path_str.push('/');
1393 }
1394 path_str.push_str(&os_str.to_string_lossy());
1395 }
1396 }
1397 path_str
1398}
1399
1400#[cfg(test)]
1401mod test {
1402 use super::{FileOptions, ZipWriter};
1403 use crate::compression::CompressionMethod;
1404 use crate::types::DateTime;
1405 use std::io;
1406 use std::io::Write;
1407
1408 #[test]
1409 fn write_empty_zip() {
1410 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1411 writer.set_comment("ZIP");
1412 let result = writer.finish().unwrap();
1413 assert_eq!(result.get_ref().len(), 25);
1414 assert_eq!(
1415 *result.get_ref(),
1416 [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]
1417 );
1418 }
1419
1420 #[test]
1421 fn unix_permissions_bitmask() {
1422 let options = FileOptions::default().unix_permissions(0o120777);
1424 assert_eq!(options.permissions, Some(0o777));
1425 }
1426
1427 #[test]
1428 fn write_zip_dir() {
1429 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1430 writer
1431 .add_directory(
1432 "test",
1433 FileOptions::default().last_modified_time(
1434 DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
1435 ),
1436 )
1437 .unwrap();
1438 assert!(writer
1439 .write(b"writing to a directory is not allowed, and will not write any data")
1440 .is_err());
1441 let result = writer.finish().unwrap();
1442 assert_eq!(result.get_ref().len(), 108);
1443 assert_eq!(
1444 *result.get_ref(),
1445 &[
1446 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1447 0, 0, 5, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 1, 2, 46, 3, 20, 0, 0, 0, 0, 0,
1448 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,
1449 0, 0, 237, 65, 0, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0,
1450 1, 0, 51, 0, 0, 0, 35, 0, 0, 0, 0, 0,
1451 ] as &[u8]
1452 );
1453 }
1454
1455 #[test]
1456 fn write_symlink_simple() {
1457 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1458 writer
1459 .add_symlink(
1460 "name",
1461 "target",
1462 FileOptions::default().last_modified_time(
1463 DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
1464 ),
1465 )
1466 .unwrap();
1467 assert!(writer
1468 .write(b"writing to a symlink is not allowed and will not write any data")
1469 .is_err());
1470 let result = writer.finish().unwrap();
1471 assert_eq!(result.get_ref().len(), 112);
1472 assert_eq!(
1473 *result.get_ref(),
1474 &[
1475 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 252, 47, 111, 70, 6, 0, 0, 0,
1476 6, 0, 0, 0, 4, 0, 0, 0, 110, 97, 109, 101, 116, 97, 114, 103, 101, 116, 80, 75, 1,
1477 2, 46, 3, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 252, 47, 111, 70, 6, 0, 0, 0, 6, 0,
1478 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 161, 0, 0, 0, 0, 110, 97, 109, 101,
1479 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 50, 0, 0, 0, 40, 0, 0, 0, 0, 0
1480 ] as &[u8],
1481 );
1482 }
1483
1484 #[test]
1485 fn write_symlink_wonky_paths() {
1486 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1487 writer
1488 .add_symlink(
1489 "directory\\link",
1490 "/absolute/symlink\\with\\mixed/slashes",
1491 FileOptions::default().last_modified_time(
1492 DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
1493 ),
1494 )
1495 .unwrap();
1496 assert!(writer
1497 .write(b"writing to a symlink is not allowed and will not write any data")
1498 .is_err());
1499 let result = writer.finish().unwrap();
1500 assert_eq!(result.get_ref().len(), 162);
1501 assert_eq!(
1502 *result.get_ref(),
1503 &[
1504 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 95, 41, 81, 245, 36, 0, 0, 0,
1505 36, 0, 0, 0, 14, 0, 0, 0, 100, 105, 114, 101, 99, 116, 111, 114, 121, 92, 108, 105,
1506 110, 107, 47, 97, 98, 115, 111, 108, 117, 116, 101, 47, 115, 121, 109, 108, 105,
1507 110, 107, 92, 119, 105, 116, 104, 92, 109, 105, 120, 101, 100, 47, 115, 108, 97,
1508 115, 104, 101, 115, 80, 75, 1, 2, 46, 3, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 95,
1509 41, 81, 245, 36, 0, 0, 0, 36, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255,
1510 161, 0, 0, 0, 0, 100, 105, 114, 101, 99, 116, 111, 114, 121, 92, 108, 105, 110,
1511 107, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 60, 0, 0, 0, 80, 0, 0, 0, 0, 0
1512 ] as &[u8],
1513 );
1514 }
1515
1516 #[test]
1517 fn write_mimetype_zip() {
1518 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1519 let options = FileOptions {
1520 compression_method: CompressionMethod::Stored,
1521 compression_level: None,
1522 last_modified_time: DateTime::default(),
1523 permissions: Some(33188),
1524 large_file: false,
1525 encrypt_with: None,
1526 };
1527 writer.start_file("mimetype", options).unwrap();
1528 writer
1529 .write_all(b"application/vnd.oasis.opendocument.text")
1530 .unwrap();
1531 let result = writer.finish().unwrap();
1532
1533 assert_eq!(result.get_ref().len(), 153);
1534 let mut v = Vec::new();
1535 v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip"));
1536 assert_eq!(result.get_ref(), &v);
1537 }
1538
1539 #[test]
1540 fn path_to_string() {
1541 let mut path = std::path::PathBuf::new();
1542 #[cfg(windows)]
1543 path.push(r"C:\");
1544 #[cfg(unix)]
1545 path.push("/");
1546 path.push("windows");
1547 path.push("..");
1548 path.push(".");
1549 path.push("system32");
1550 let path_str = super::path_to_string(&path);
1551 assert_eq!(path_str, "windows/system32");
1552 }
1553}
1554
1555#[cfg(not(feature = "unreserved"))]
1556const EXTRA_FIELD_MAPPING: [u16; 49] = [
1557 0x0001, 0x0007, 0x0008, 0x0009, 0x000a, 0x000c, 0x000d, 0x000e, 0x000f, 0x0014, 0x0015, 0x0016,
1558 0x0017, 0x0018, 0x0019, 0x0020, 0x0021, 0x0022, 0x0023, 0x0065, 0x0066, 0x4690, 0x07c8, 0x2605,
1559 0x2705, 0x2805, 0x334d, 0x4341, 0x4453, 0x4704, 0x470f, 0x4b46, 0x4c41, 0x4d49, 0x4f4c, 0x5356,
1560 0x5455, 0x554e, 0x5855, 0x6375, 0x6542, 0x7075, 0x756e, 0x7855, 0xa11e, 0xa220, 0xfd4a, 0x9901,
1561 0x9902,
1562];