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 crc32fast::Hasher;
10use std::default::Default;
11use std::io;
12use std::io::prelude::*;
13use std::mem;
14
15#[cfg(any(
16 feature = "deflate",
17 feature = "deflate-miniz",
18 feature = "deflate-zlib"
19))]
20use flate2::write::DeflateEncoder;
21
22#[cfg(feature = "bzip2")]
23use bzip2::write::BzEncoder;
24
25#[cfg(feature = "time")]
26use time::OffsetDateTime;
27
28#[cfg(feature = "zstd")]
29use zstd::stream::write::Encoder as ZstdEncoder;
30
31enum GenericZipWriter<W: Write + io::Seek> {
32 Closed,
33 Storer(W),
34 #[cfg(any(
35 feature = "deflate",
36 feature = "deflate-miniz",
37 feature = "deflate-zlib"
38 ))]
39 Deflater(DeflateEncoder<W>),
40 #[cfg(feature = "bzip2")]
41 Bzip2(BzEncoder<W>),
42 #[cfg(feature = "zstd")]
43 Zstd(ZstdEncoder<'static, W>),
44}
45pub(crate) mod zip_writer {
47 use super::*;
48 pub struct ZipWriter<W: Write + io::Seek> {
77 pub(super) inner: GenericZipWriter<W>,
78 pub(super) files: Vec<ZipFileData>,
79 pub(super) stats: ZipWriterStats,
80 pub(super) writing_to_file: bool,
81 pub(super) writing_to_extra_field: bool,
82 pub(super) writing_to_central_extra_field_only: bool,
83 pub(super) writing_raw: bool,
84 pub(super) comment: Vec<u8>,
85 }
86}
87pub use zip_writer::ZipWriter;
88
89#[derive(Default)]
90struct ZipWriterStats {
91 hasher: Hasher,
92 start: u64,
93 bytes_written: u64,
94}
95
96struct ZipRawValues {
97 crc32: u32,
98 compressed_size: u64,
99 uncompressed_size: u64,
100}
101
102#[derive(Copy, Clone)]
104pub struct FileOptions {
105 compression_method: CompressionMethod,
106 compression_level: Option<i32>,
107 last_modified_time: DateTime,
108 permissions: Option<u32>,
109 large_file: bool,
110}
111
112impl FileOptions {
113 pub fn default() -> FileOptions {
115 FileOptions {
116 #[cfg(any(
117 feature = "deflate",
118 feature = "deflate-miniz",
119 feature = "deflate-zlib"
120 ))]
121 compression_method: CompressionMethod::Deflated,
122 #[cfg(not(any(
123 feature = "deflate",
124 feature = "deflate-miniz",
125 feature = "deflate-zlib"
126 )))]
127 compression_method: CompressionMethod::Stored,
128 compression_level: None,
129 #[cfg(feature = "time")]
130 last_modified_time: DateTime::from_time(OffsetDateTime::now_utc()).unwrap_or_default(),
131 #[cfg(not(feature = "time"))]
132 last_modified_time: DateTime::default(),
133 permissions: None,
134 large_file: false,
135 }
136 }
137
138 #[must_use]
143 pub fn compression_method(mut self, method: CompressionMethod) -> FileOptions {
144 self.compression_method = method;
145 self
146 }
147
148 #[must_use]
158 pub fn compression_level(mut self, level: Option<i32>) -> FileOptions {
159 self.compression_level = level;
160 self
161 }
162
163 #[must_use]
168 pub fn last_modified_time(mut self, mod_time: DateTime) -> FileOptions {
169 self.last_modified_time = mod_time;
170 self
171 }
172
173 #[must_use]
183 pub fn unix_permissions(mut self, mode: u32) -> FileOptions {
184 self.permissions = Some(mode & 0o777);
185 self
186 }
187
188 #[must_use]
194 pub fn large_file(mut self, large: bool) -> FileOptions {
195 self.large_file = large;
196 self
197 }
198}
199
200impl Default for FileOptions {
201 fn default() -> Self {
202 Self::default()
203 }
204}
205
206impl<W: Write + io::Seek> Write for ZipWriter<W> {
207 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
208 if !self.writing_to_file {
209 return Err(io::Error::new(
210 io::ErrorKind::Other,
211 "No file has been started",
212 ));
213 }
214 match self.inner.ref_mut() {
215 Some(ref mut w) => {
216 if self.writing_to_extra_field {
217 self.files.last_mut().unwrap().extra_field.write(buf)
218 } else {
219 let write_result = w.write(buf);
220 if let Ok(count) = write_result {
221 self.stats.update(&buf[0..count]);
222 if self.stats.bytes_written > spec::ZIP64_BYTES_THR
223 && !self.files.last_mut().unwrap().large_file
224 {
225 let _inner = mem::replace(&mut self.inner, GenericZipWriter::Closed);
226 return Err(io::Error::new(
227 io::ErrorKind::Other,
228 "Large file option has not been set",
229 ));
230 }
231 }
232 write_result
233 }
234 }
235 None => Err(io::Error::new(
236 io::ErrorKind::BrokenPipe,
237 "ZipWriter was already closed",
238 )),
239 }
240 }
241
242 fn flush(&mut self) -> io::Result<()> {
243 match self.inner.ref_mut() {
244 Some(ref mut w) => w.flush(),
245 None => Err(io::Error::new(
246 io::ErrorKind::BrokenPipe,
247 "ZipWriter was already closed",
248 )),
249 }
250 }
251}
252
253impl ZipWriterStats {
254 fn update(&mut self, buf: &[u8]) {
255 self.hasher.update(buf);
256 self.bytes_written += buf.len() as u64;
257 }
258}
259
260impl<A: Read + Write + io::Seek> ZipWriter<A> {
261 pub fn new_append(mut readwriter: A) -> ZipResult<ZipWriter<A>> {
263 let (footer, cde_start_pos) = spec::CentralDirectoryEnd::find_and_parse(&mut readwriter)?;
264
265 if footer.disk_number != footer.disk_with_central_directory {
266 return Err(ZipError::UnsupportedArchive(
267 "Support for multi-disk files is not implemented",
268 ));
269 }
270
271 let (archive_offset, directory_start, number_of_files) =
272 ZipArchive::get_directory_counts(&mut readwriter, &footer, cde_start_pos)?;
273
274 if readwriter
275 .seek(io::SeekFrom::Start(directory_start))
276 .is_err()
277 {
278 return Err(ZipError::InvalidArchive(
279 "Could not seek to start of central directory",
280 ));
281 }
282
283 let files = (0..number_of_files)
284 .map(|_| central_header_to_zip_file(&mut readwriter, archive_offset))
285 .collect::<Result<Vec<_>, _>>()?;
286
287 let _ = readwriter.seek(io::SeekFrom::Start(directory_start)); Ok(ZipWriter {
290 inner: GenericZipWriter::Storer(readwriter),
291 files,
292 stats: Default::default(),
293 writing_to_file: false,
294 writing_to_extra_field: false,
295 writing_to_central_extra_field_only: false,
296 comment: footer.zip_file_comment,
297 writing_raw: true, })
299 }
300}
301
302impl<W: Write + io::Seek> ZipWriter<W> {
303 pub fn new(inner: W) -> ZipWriter<W> {
307 ZipWriter {
308 inner: GenericZipWriter::Storer(inner),
309 files: Vec::new(),
310 stats: Default::default(),
311 writing_to_file: false,
312 writing_to_extra_field: false,
313 writing_to_central_extra_field_only: false,
314 writing_raw: false,
315 comment: Vec::new(),
316 }
317 }
318
319 pub fn set_comment<S>(&mut self, comment: S)
321 where
322 S: Into<String>,
323 {
324 self.set_raw_comment(comment.into().into())
325 }
326
327 pub fn set_raw_comment(&mut self, comment: Vec<u8>) {
332 self.comment = comment;
333 }
334
335 fn start_entry<S>(
337 &mut self,
338 name: S,
339 options: FileOptions,
340 raw_values: Option<ZipRawValues>,
341 ) -> ZipResult<()>
342 where
343 S: Into<String>,
344 {
345 self.finish_file()?;
346
347 let raw_values = raw_values.unwrap_or(ZipRawValues {
348 crc32: 0,
349 compressed_size: 0,
350 uncompressed_size: 0,
351 });
352
353 {
354 let writer = self.inner.get_plain();
355 let header_start = writer.stream_position()?;
356
357 let permissions = options.permissions.unwrap_or(0o100644);
358 let mut file = ZipFileData {
359 system: System::Unix,
360 version_made_by: DEFAULT_VERSION,
361 encrypted: false,
362 using_data_descriptor: false,
363 compression_method: options.compression_method,
364 compression_level: options.compression_level,
365 last_modified_time: options.last_modified_time,
366 crc32: raw_values.crc32,
367 compressed_size: raw_values.compressed_size,
368 uncompressed_size: raw_values.uncompressed_size,
369 file_name: name.into(),
370 file_name_raw: Vec::new(), extra_field: Vec::new(),
372 file_comment: String::new(),
373 header_start,
374 data_start: AtomicU64::new(0),
375 central_header_start: 0,
376 external_attributes: permissions << 16,
377 large_file: options.large_file,
378 aes_mode: None,
379 };
380 write_local_file_header(writer, &file)?;
381
382 let header_end = writer.stream_position()?;
383 self.stats.start = header_end;
384 *file.data_start.get_mut() = header_end;
385
386 self.stats.bytes_written = 0;
387 self.stats.hasher = Hasher::new();
388
389 self.files.push(file);
390 }
391
392 Ok(())
393 }
394
395 fn finish_file(&mut self) -> ZipResult<()> {
396 if self.writing_to_extra_field {
397 self.end_extra_data()?;
399 }
400 self.inner.switch_to(CompressionMethod::Stored, None)?;
401 let writer = self.inner.get_plain();
402
403 if !self.writing_raw {
404 let file = match self.files.last_mut() {
405 None => return Ok(()),
406 Some(f) => f,
407 };
408 file.crc32 = self.stats.hasher.clone().finalize();
409 file.uncompressed_size = self.stats.bytes_written;
410
411 let file_end = writer.stream_position()?;
412 file.compressed_size = file_end - self.stats.start;
413
414 update_local_file_header(writer, file)?;
415 writer.seek(io::SeekFrom::Start(file_end))?;
416 }
417
418 self.writing_to_file = false;
419 self.writing_raw = false;
420 Ok(())
421 }
422
423 pub fn start_file<S>(&mut self, name: S, mut options: FileOptions) -> ZipResult<()>
427 where
428 S: Into<String>,
429 {
430 if options.permissions.is_none() {
431 options.permissions = Some(0o644);
432 }
433 *options.permissions.as_mut().unwrap() |= 0o100000;
434 self.start_entry(name, options, None)?;
435 self.inner
436 .switch_to(options.compression_method, options.compression_level)?;
437 self.writing_to_file = true;
438 Ok(())
439 }
440
441 #[deprecated(
446 since = "0.5.7",
447 note = "by stripping `..`s from the path, the meaning of paths can change. Use `start_file` instead."
448 )]
449 pub fn start_file_from_path(
450 &mut self,
451 path: &std::path::Path,
452 options: FileOptions,
453 ) -> ZipResult<()> {
454 self.start_file(path_to_string(path), options)
455 }
456
457 pub fn start_file_aligned<S>(
463 &mut self,
464 name: S,
465 options: FileOptions,
466 align: u16,
467 ) -> Result<u64, ZipError>
468 where
469 S: Into<String>,
470 {
471 let data_start = self.start_file_with_extra_data(name, options)?;
472 let align = align as u64;
473 if align > 1 && data_start % align != 0 {
474 let pad_length = (align - (data_start + 4) % align) % align;
475 let pad = vec![0; pad_length as usize];
476 self.write_all(b"za").map_err(ZipError::from)?; self.write_u16::<LittleEndian>(pad.len() as u16)
478 .map_err(ZipError::from)?;
479 self.write_all(&pad).map_err(ZipError::from)?;
480 assert_eq!(self.end_local_start_central_extra_data()? % align, 0);
481 }
482 let extra_data_end = self.end_extra_data()?;
483 Ok(extra_data_end - data_start)
484 }
485
486 pub fn start_file_with_extra_data<S>(
550 &mut self,
551 name: S,
552 mut options: FileOptions,
553 ) -> ZipResult<u64>
554 where
555 S: Into<String>,
556 {
557 if options.permissions.is_none() {
558 options.permissions = Some(0o644);
559 }
560 *options.permissions.as_mut().unwrap() |= 0o100000;
561 self.start_entry(name, options, None)?;
562 self.writing_to_file = true;
563 self.writing_to_extra_field = true;
564 Ok(self.files.last().unwrap().data_start.load())
565 }
566
567 pub fn end_local_start_central_extra_data(&mut self) -> ZipResult<u64> {
571 let data_start = self.end_extra_data()?;
572 self.files.last_mut().unwrap().extra_field.clear();
573 self.writing_to_extra_field = true;
574 self.writing_to_central_extra_field_only = true;
575 Ok(data_start)
576 }
577
578 pub fn end_extra_data(&mut self) -> ZipResult<u64> {
582 if !self.writing_to_extra_field {
584 return Err(ZipError::Io(io::Error::new(
585 io::ErrorKind::Other,
586 "Not writing to extra field",
587 )));
588 }
589 let file = self.files.last_mut().unwrap();
590
591 validate_extra_data(file)?;
592
593 let data_start = file.data_start.get_mut();
594
595 if !self.writing_to_central_extra_field_only {
596 let writer = self.inner.get_plain();
597
598 writer.write_all(&file.extra_field)?;
600
601 let header_end = *data_start + file.extra_field.len() as u64;
603 self.stats.start = header_end;
604 *data_start = header_end;
605
606 let extra_field_length =
608 if file.large_file { 20 } else { 0 } + file.extra_field.len() as u16;
609 writer.seek(io::SeekFrom::Start(file.header_start + 28))?;
610 writer.write_u16::<LittleEndian>(extra_field_length)?;
611 writer.seek(io::SeekFrom::Start(header_end))?;
612
613 self.inner
614 .switch_to(file.compression_method, file.compression_level)?;
615 }
616
617 self.writing_to_extra_field = false;
618 self.writing_to_central_extra_field_only = false;
619 Ok(*data_start)
620 }
621
622 pub fn raw_copy_file_rename<S>(&mut self, mut file: ZipFile, name: S) -> ZipResult<()>
649 where
650 S: Into<String>,
651 {
652 let mut options = FileOptions::default()
653 .large_file(file.compressed_size().max(file.size()) > spec::ZIP64_BYTES_THR)
654 .last_modified_time(file.last_modified())
655 .compression_method(file.compression());
656 if let Some(perms) = file.unix_mode() {
657 options = options.unix_permissions(perms);
658 }
659
660 let raw_values = ZipRawValues {
661 crc32: file.crc32(),
662 compressed_size: file.compressed_size(),
663 uncompressed_size: file.size(),
664 };
665
666 self.start_entry(name, options, Some(raw_values))?;
667 self.writing_to_file = true;
668 self.writing_raw = true;
669
670 io::copy(file.get_raw_reader(), self)?;
671
672 Ok(())
673 }
674
675 pub fn raw_copy_file(&mut self, file: ZipFile) -> ZipResult<()> {
699 let name = file.name().to_owned();
700 self.raw_copy_file_rename(file, name)
701 }
702
703 pub fn add_directory<S>(&mut self, name: S, mut options: FileOptions) -> ZipResult<()>
707 where
708 S: Into<String>,
709 {
710 if options.permissions.is_none() {
711 options.permissions = Some(0o755);
712 }
713 *options.permissions.as_mut().unwrap() |= 0o40000;
714 options.compression_method = CompressionMethod::Stored;
715
716 let name_as_string = name.into();
717 let name_with_slash = match name_as_string.chars().last() {
719 Some('/') | Some('\\') => name_as_string,
720 _ => name_as_string + "/",
721 };
722
723 self.start_entry(name_with_slash, options, None)?;
724 self.writing_to_file = false;
725 Ok(())
726 }
727
728 #[deprecated(
733 since = "0.5.7",
734 note = "by stripping `..`s from the path, the meaning of paths can change. Use `add_directory` instead."
735 )]
736 pub fn add_directory_from_path(
737 &mut self,
738 path: &std::path::Path,
739 options: FileOptions,
740 ) -> ZipResult<()> {
741 self.add_directory(path_to_string(path), options)
742 }
743
744 pub fn finish(&mut self) -> ZipResult<W> {
749 self.finalize()?;
750 let inner = mem::replace(&mut self.inner, GenericZipWriter::Closed);
751 Ok(inner.unwrap())
752 }
753
754 pub fn add_symlink<N, T>(
767 &mut self,
768 name: N,
769 target: T,
770 mut options: FileOptions,
771 ) -> ZipResult<()>
772 where
773 N: Into<String>,
774 T: Into<String>,
775 {
776 if options.permissions.is_none() {
777 options.permissions = Some(0o777);
778 }
779 *options.permissions.as_mut().unwrap() |= 0o120000;
780 options.compression_method = CompressionMethod::Stored;
783
784 self.start_entry(name, options, None)?;
785 self.writing_to_file = true;
786 self.write_all(target.into().as_bytes())?;
787 self.writing_to_file = false;
788
789 Ok(())
790 }
791
792 fn finalize(&mut self) -> ZipResult<()> {
793 self.finish_file()?;
794
795 {
796 let writer = self.inner.get_plain();
797
798 let central_start = writer.stream_position()?;
799 for file in self.files.iter() {
800 write_central_directory_header(writer, file)?;
801 }
802 let central_size = writer.stream_position()? - central_start;
803
804 if self.files.len() > spec::ZIP64_ENTRY_THR
805 || central_size.max(central_start) > spec::ZIP64_BYTES_THR
806 {
807 let zip64_footer = spec::Zip64CentralDirectoryEnd {
808 version_made_by: DEFAULT_VERSION as u16,
809 version_needed_to_extract: DEFAULT_VERSION as u16,
810 disk_number: 0,
811 disk_with_central_directory: 0,
812 number_of_files_on_this_disk: self.files.len() as u64,
813 number_of_files: self.files.len() as u64,
814 central_directory_size: central_size,
815 central_directory_offset: central_start,
816 };
817
818 zip64_footer.write(writer)?;
819
820 let zip64_footer = spec::Zip64CentralDirectoryEndLocator {
821 disk_with_central_directory: 0,
822 end_of_central_directory_offset: central_start + central_size,
823 number_of_disks: 1,
824 };
825
826 zip64_footer.write(writer)?;
827 }
828
829 let number_of_files = self.files.len().min(spec::ZIP64_ENTRY_THR) as u16;
830 let footer = spec::CentralDirectoryEnd {
831 disk_number: 0,
832 disk_with_central_directory: 0,
833 zip_file_comment: self.comment.clone(),
834 number_of_files_on_this_disk: number_of_files,
835 number_of_files,
836 central_directory_size: central_size.min(spec::ZIP64_BYTES_THR) as u32,
837 central_directory_offset: central_start.min(spec::ZIP64_BYTES_THR) as u32,
838 };
839
840 footer.write(writer)?;
841 }
842
843 Ok(())
844 }
845}
846
847impl<W: Write + io::Seek> Drop for ZipWriter<W> {
848 fn drop(&mut self) {
849 if !self.inner.is_closed() {
850 if let Err(e) = self.finalize() {
851 let _ = write!(io::stderr(), "ZipWriter drop failed: {:?}", e);
852 }
853 }
854 }
855}
856
857impl<W: Write + io::Seek> GenericZipWriter<W> {
858 fn switch_to(
859 &mut self,
860 compression: CompressionMethod,
861 compression_level: Option<i32>,
862 ) -> ZipResult<()> {
863 match self.current_compression() {
864 Some(method) if method == compression => return Ok(()),
865 None => {
866 return Err(io::Error::new(
867 io::ErrorKind::BrokenPipe,
868 "ZipWriter was already closed",
869 )
870 .into())
871 }
872 _ => {}
873 }
874
875 let bare = match mem::replace(self, GenericZipWriter::Closed) {
876 GenericZipWriter::Storer(w) => w,
877 #[cfg(any(
878 feature = "deflate",
879 feature = "deflate-miniz",
880 feature = "deflate-zlib"
881 ))]
882 GenericZipWriter::Deflater(w) => w.finish()?,
883 #[cfg(feature = "bzip2")]
884 GenericZipWriter::Bzip2(w) => w.finish()?,
885 #[cfg(feature = "zstd")]
886 GenericZipWriter::Zstd(w) => w.finish()?,
887 GenericZipWriter::Closed => {
888 return Err(io::Error::new(
889 io::ErrorKind::BrokenPipe,
890 "ZipWriter was already closed",
891 )
892 .into())
893 }
894 };
895
896 *self = {
897 #[allow(deprecated)]
898 match compression {
899 CompressionMethod::Stored => {
900 if compression_level.is_some() {
901 return Err(ZipError::UnsupportedArchive(
902 "Unsupported compression level",
903 ));
904 }
905
906 GenericZipWriter::Storer(bare)
907 }
908 #[cfg(any(
909 feature = "deflate",
910 feature = "deflate-miniz",
911 feature = "deflate-zlib"
912 ))]
913 CompressionMethod::Deflated => GenericZipWriter::Deflater(DeflateEncoder::new(
914 bare,
915 flate2::Compression::new(
916 clamp_opt(
917 compression_level
918 .unwrap_or(flate2::Compression::default().level() as i32),
919 deflate_compression_level_range(),
920 )
921 .ok_or(ZipError::UnsupportedArchive(
922 "Unsupported compression level",
923 ))? as u32,
924 ),
925 )),
926 #[cfg(feature = "bzip2")]
927 CompressionMethod::Bzip2 => GenericZipWriter::Bzip2(BzEncoder::new(
928 bare,
929 bzip2::Compression::new(
930 clamp_opt(
931 compression_level
932 .unwrap_or(bzip2::Compression::default().level() as i32),
933 bzip2_compression_level_range(),
934 )
935 .ok_or(ZipError::UnsupportedArchive(
936 "Unsupported compression level",
937 ))? as u32,
938 ),
939 )),
940 CompressionMethod::AES => {
941 return Err(ZipError::UnsupportedArchive(
942 "AES compression is not supported for writing",
943 ))
944 }
945 #[cfg(feature = "zstd")]
946 CompressionMethod::Zstd => GenericZipWriter::Zstd(
947 ZstdEncoder::new(
948 bare,
949 clamp_opt(
950 compression_level.unwrap_or(zstd::DEFAULT_COMPRESSION_LEVEL),
951 zstd::compression_level_range(),
952 )
953 .ok_or(ZipError::UnsupportedArchive(
954 "Unsupported compression level",
955 ))?,
956 )
957 .unwrap(),
958 ),
959 CompressionMethod::Unsupported(..) => {
960 return Err(ZipError::UnsupportedArchive("Unsupported compression"))
961 }
962 }
963 };
964
965 Ok(())
966 }
967
968 fn ref_mut(&mut self) -> Option<&mut dyn Write> {
969 match *self {
970 GenericZipWriter::Storer(ref mut w) => Some(w as &mut dyn Write),
971 #[cfg(any(
972 feature = "deflate",
973 feature = "deflate-miniz",
974 feature = "deflate-zlib"
975 ))]
976 GenericZipWriter::Deflater(ref mut w) => Some(w as &mut dyn Write),
977 #[cfg(feature = "bzip2")]
978 GenericZipWriter::Bzip2(ref mut w) => Some(w as &mut dyn Write),
979 #[cfg(feature = "zstd")]
980 GenericZipWriter::Zstd(ref mut w) => Some(w as &mut dyn Write),
981 GenericZipWriter::Closed => None,
982 }
983 }
984
985 fn is_closed(&self) -> bool {
986 matches!(*self, GenericZipWriter::Closed)
987 }
988
989 fn get_plain(&mut self) -> &mut W {
990 match *self {
991 GenericZipWriter::Storer(ref mut w) => w,
992 _ => panic!("Should have switched to stored beforehand"),
993 }
994 }
995
996 fn current_compression(&self) -> Option<CompressionMethod> {
997 match *self {
998 GenericZipWriter::Storer(..) => Some(CompressionMethod::Stored),
999 #[cfg(any(
1000 feature = "deflate",
1001 feature = "deflate-miniz",
1002 feature = "deflate-zlib"
1003 ))]
1004 GenericZipWriter::Deflater(..) => Some(CompressionMethod::Deflated),
1005 #[cfg(feature = "bzip2")]
1006 GenericZipWriter::Bzip2(..) => Some(CompressionMethod::Bzip2),
1007 #[cfg(feature = "zstd")]
1008 GenericZipWriter::Zstd(..) => Some(CompressionMethod::Zstd),
1009 GenericZipWriter::Closed => None,
1010 }
1011 }
1012
1013 fn unwrap(self) -> W {
1014 match self {
1015 GenericZipWriter::Storer(w) => w,
1016 _ => panic!("Should have switched to stored beforehand"),
1017 }
1018 }
1019}
1020
1021#[cfg(any(
1022 feature = "deflate",
1023 feature = "deflate-miniz",
1024 feature = "deflate-zlib"
1025))]
1026fn deflate_compression_level_range() -> std::ops::RangeInclusive<i32> {
1027 let min = flate2::Compression::none().level() as i32;
1028 let max = flate2::Compression::best().level() as i32;
1029 min..=max
1030}
1031
1032#[cfg(feature = "bzip2")]
1033fn bzip2_compression_level_range() -> std::ops::RangeInclusive<i32> {
1034 let min = bzip2::Compression::none().level() as i32;
1035 let max = bzip2::Compression::best().level() as i32;
1036 min..=max
1037}
1038
1039#[cfg(any(
1040 feature = "deflate",
1041 feature = "deflate-miniz",
1042 feature = "deflate-zlib",
1043 feature = "bzip2",
1044 feature = "zstd"
1045))]
1046fn clamp_opt<T: Ord + Copy>(value: T, range: std::ops::RangeInclusive<T>) -> Option<T> {
1047 if range.contains(&value) {
1048 Some(value)
1049 } else {
1050 None
1051 }
1052}
1053
1054fn write_local_file_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
1055 writer.write_u32::<LittleEndian>(spec::LOCAL_FILE_HEADER_SIGNATURE)?;
1057 writer.write_u16::<LittleEndian>(file.version_needed())?;
1059 let flag = if !file.file_name.is_ascii() {
1061 1u16 << 11
1062 } else {
1063 0
1064 };
1065 writer.write_u16::<LittleEndian>(flag)?;
1066 #[allow(deprecated)]
1068 writer.write_u16::<LittleEndian>(file.compression_method.to_u16())?;
1069 writer.write_u16::<LittleEndian>(file.last_modified_time.timepart())?;
1071 writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?;
1072 writer.write_u32::<LittleEndian>(file.crc32)?;
1074 if file.large_file {
1076 writer.write_u32::<LittleEndian>(spec::ZIP64_BYTES_THR as u32)?;
1077 writer.write_u32::<LittleEndian>(spec::ZIP64_BYTES_THR as u32)?;
1078 } else {
1079 writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
1080 writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
1081 }
1082 writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
1084 let extra_field_length = if file.large_file { 20 } else { 0 } + file.extra_field.len() as u16;
1086 writer.write_u16::<LittleEndian>(extra_field_length)?;
1087 writer.write_all(file.file_name.as_bytes())?;
1089 if file.large_file {
1091 write_local_zip64_extra_field(writer, file)?;
1092 }
1093
1094 Ok(())
1095}
1096
1097fn update_local_file_header<T: Write + io::Seek>(
1098 writer: &mut T,
1099 file: &ZipFileData,
1100) -> ZipResult<()> {
1101 const CRC32_OFFSET: u64 = 14;
1102 writer.seek(io::SeekFrom::Start(file.header_start + CRC32_OFFSET))?;
1103 writer.write_u32::<LittleEndian>(file.crc32)?;
1104 if file.large_file {
1105 update_local_zip64_extra_field(writer, file)?;
1106 } else {
1107 if file.compressed_size > spec::ZIP64_BYTES_THR {
1109 return Err(ZipError::Io(io::Error::new(
1110 io::ErrorKind::Other,
1111 "Large file option has not been set",
1112 )));
1113 }
1114 writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
1115 writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
1117 }
1118 Ok(())
1119}
1120
1121fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
1122 let mut zip64_extra_field = [0; 28];
1124 let zip64_extra_field_length =
1125 write_central_zip64_extra_field(&mut zip64_extra_field.as_mut(), file)?;
1126
1127 writer.write_u32::<LittleEndian>(spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE)?;
1129 let version_made_by = (file.system as u16) << 8 | (file.version_made_by as u16);
1131 writer.write_u16::<LittleEndian>(version_made_by)?;
1132 writer.write_u16::<LittleEndian>(file.version_needed())?;
1134 let flag = if !file.file_name.is_ascii() {
1136 1u16 << 11
1137 } else {
1138 0
1139 };
1140 writer.write_u16::<LittleEndian>(flag)?;
1141 #[allow(deprecated)]
1143 writer.write_u16::<LittleEndian>(file.compression_method.to_u16())?;
1144 writer.write_u16::<LittleEndian>(file.last_modified_time.timepart())?;
1146 writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?;
1147 writer.write_u32::<LittleEndian>(file.crc32)?;
1149 writer.write_u32::<LittleEndian>(file.compressed_size.min(spec::ZIP64_BYTES_THR) as u32)?;
1151 writer.write_u32::<LittleEndian>(file.uncompressed_size.min(spec::ZIP64_BYTES_THR) as u32)?;
1153 writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
1155 writer.write_u16::<LittleEndian>(zip64_extra_field_length + file.extra_field.len() as u16)?;
1157 writer.write_u16::<LittleEndian>(0)?;
1159 writer.write_u16::<LittleEndian>(0)?;
1161 writer.write_u16::<LittleEndian>(0)?;
1163 writer.write_u32::<LittleEndian>(file.external_attributes)?;
1165 writer.write_u32::<LittleEndian>(file.header_start.min(spec::ZIP64_BYTES_THR) as u32)?;
1167 writer.write_all(file.file_name.as_bytes())?;
1169 writer.write_all(&zip64_extra_field[..zip64_extra_field_length as usize])?;
1171 writer.write_all(&file.extra_field)?;
1173 Ok(())
1177}
1178
1179fn validate_extra_data(file: &ZipFileData) -> ZipResult<()> {
1180 let mut data = file.extra_field.as_slice();
1181
1182 if data.len() > spec::ZIP64_ENTRY_THR {
1183 return Err(ZipError::Io(io::Error::new(
1184 io::ErrorKind::InvalidData,
1185 "Extra data exceeds extra field",
1186 )));
1187 }
1188
1189 while !data.is_empty() {
1190 let left = data.len();
1191 if left < 4 {
1192 return Err(ZipError::Io(io::Error::new(
1193 io::ErrorKind::Other,
1194 "Incomplete extra data header",
1195 )));
1196 }
1197 let kind = data.read_u16::<LittleEndian>()?;
1198 let size = data.read_u16::<LittleEndian>()? as usize;
1199 let left = left - 4;
1200
1201 if kind == 0x0001 {
1202 return Err(ZipError::Io(io::Error::new(
1203 io::ErrorKind::Other,
1204 "No custom ZIP64 extra data allowed",
1205 )));
1206 }
1207
1208 #[cfg(not(feature = "unreserved"))]
1209 {
1210 if kind <= 31 || EXTRA_FIELD_MAPPING.iter().any(|&mapped| mapped == kind) {
1211 return Err(ZipError::Io(io::Error::new(
1212 io::ErrorKind::Other,
1213 format!(
1214 "Extra data header ID {:#06} requires crate feature \"unreserved\"",
1215 kind,
1216 ),
1217 )));
1218 }
1219 }
1220
1221 if size > left {
1222 return Err(ZipError::Io(io::Error::new(
1223 io::ErrorKind::Other,
1224 "Extra data size exceeds extra field",
1225 )));
1226 }
1227
1228 data = &data[size..];
1229 }
1230
1231 Ok(())
1232}
1233
1234fn write_local_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
1235 writer.write_u16::<LittleEndian>(0x0001)?;
1238 writer.write_u16::<LittleEndian>(16)?;
1239 writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
1240 writer.write_u64::<LittleEndian>(file.compressed_size)?;
1241 Ok(())
1244}
1245
1246fn update_local_zip64_extra_field<T: Write + io::Seek>(
1247 writer: &mut T,
1248 file: &ZipFileData,
1249) -> ZipResult<()> {
1250 let zip64_extra_field = file.header_start + 30 + file.file_name.as_bytes().len() as u64;
1251 writer.seek(io::SeekFrom::Start(zip64_extra_field + 4))?;
1252 writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
1253 writer.write_u64::<LittleEndian>(file.compressed_size)?;
1254 Ok(())
1257}
1258
1259fn write_central_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<u16> {
1260 let mut size = 0;
1265 let uncompressed_size = file.uncompressed_size > spec::ZIP64_BYTES_THR;
1266 let compressed_size = file.compressed_size > spec::ZIP64_BYTES_THR;
1267 let header_start = file.header_start > spec::ZIP64_BYTES_THR;
1268 if uncompressed_size {
1269 size += 8;
1270 }
1271 if compressed_size {
1272 size += 8;
1273 }
1274 if header_start {
1275 size += 8;
1276 }
1277 if size > 0 {
1278 writer.write_u16::<LittleEndian>(0x0001)?;
1279 writer.write_u16::<LittleEndian>(size)?;
1280 size += 4;
1281
1282 if uncompressed_size {
1283 writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
1284 }
1285 if compressed_size {
1286 writer.write_u64::<LittleEndian>(file.compressed_size)?;
1287 }
1288 if header_start {
1289 writer.write_u64::<LittleEndian>(file.header_start)?;
1290 }
1291 }
1294 Ok(size)
1295}
1296
1297fn path_to_string(path: &std::path::Path) -> String {
1298 let mut path_str = String::new();
1299 for component in path.components() {
1300 if let std::path::Component::Normal(os_str) = component {
1301 if !path_str.is_empty() {
1302 path_str.push('/');
1303 }
1304 path_str.push_str(&*os_str.to_string_lossy());
1305 }
1306 }
1307 path_str
1308}
1309
1310#[cfg(test)]
1311mod test {
1312 use super::{FileOptions, ZipWriter};
1313 use crate::compression::CompressionMethod;
1314 use crate::types::DateTime;
1315 use std::io;
1316 use std::io::Write;
1317
1318 #[test]
1319 fn write_empty_zip() {
1320 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1321 writer.set_comment("ZIP");
1322 let result = writer.finish().unwrap();
1323 assert_eq!(result.get_ref().len(), 25);
1324 assert_eq!(
1325 *result.get_ref(),
1326 [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]
1327 );
1328 }
1329
1330 #[test]
1331 fn unix_permissions_bitmask() {
1332 let options = FileOptions::default().unix_permissions(0o120777);
1334 assert_eq!(options.permissions, Some(0o777));
1335 }
1336
1337 #[test]
1338 fn write_zip_dir() {
1339 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1340 writer
1341 .add_directory(
1342 "test",
1343 FileOptions::default().last_modified_time(
1344 DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
1345 ),
1346 )
1347 .unwrap();
1348 assert!(writer
1349 .write(b"writing to a directory is not allowed, and will not write any data")
1350 .is_err());
1351 let result = writer.finish().unwrap();
1352 assert_eq!(result.get_ref().len(), 108);
1353 assert_eq!(
1354 *result.get_ref(),
1355 &[
1356 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1357 0, 0, 5, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 1, 2, 46, 3, 20, 0, 0, 0, 0, 0,
1358 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,
1359 0, 0, 237, 65, 0, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0,
1360 1, 0, 51, 0, 0, 0, 35, 0, 0, 0, 0, 0,
1361 ] as &[u8]
1362 );
1363 }
1364
1365 #[test]
1366 fn write_symlink_simple() {
1367 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1368 writer
1369 .add_symlink(
1370 "name",
1371 "target",
1372 FileOptions::default().last_modified_time(
1373 DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
1374 ),
1375 )
1376 .unwrap();
1377 assert!(writer
1378 .write(b"writing to a symlink is not allowed and will not write any data")
1379 .is_err());
1380 let result = writer.finish().unwrap();
1381 assert_eq!(result.get_ref().len(), 112);
1382 assert_eq!(
1383 *result.get_ref(),
1384 &[
1385 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 252, 47, 111, 70, 6, 0, 0, 0,
1386 6, 0, 0, 0, 4, 0, 0, 0, 110, 97, 109, 101, 116, 97, 114, 103, 101, 116, 80, 75, 1,
1387 2, 46, 3, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 252, 47, 111, 70, 6, 0, 0, 0, 6, 0,
1388 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 161, 0, 0, 0, 0, 110, 97, 109, 101,
1389 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 50, 0, 0, 0, 40, 0, 0, 0, 0, 0
1390 ] as &[u8],
1391 );
1392 }
1393
1394 #[test]
1395 fn write_symlink_wonky_paths() {
1396 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1397 writer
1398 .add_symlink(
1399 "directory\\link",
1400 "/absolute/symlink\\with\\mixed/slashes",
1401 FileOptions::default().last_modified_time(
1402 DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
1403 ),
1404 )
1405 .unwrap();
1406 assert!(writer
1407 .write(b"writing to a symlink is not allowed and will not write any data")
1408 .is_err());
1409 let result = writer.finish().unwrap();
1410 assert_eq!(result.get_ref().len(), 162);
1411 assert_eq!(
1412 *result.get_ref(),
1413 &[
1414 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 95, 41, 81, 245, 36, 0, 0, 0,
1415 36, 0, 0, 0, 14, 0, 0, 0, 100, 105, 114, 101, 99, 116, 111, 114, 121, 92, 108, 105,
1416 110, 107, 47, 97, 98, 115, 111, 108, 117, 116, 101, 47, 115, 121, 109, 108, 105,
1417 110, 107, 92, 119, 105, 116, 104, 92, 109, 105, 120, 101, 100, 47, 115, 108, 97,
1418 115, 104, 101, 115, 80, 75, 1, 2, 46, 3, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 95,
1419 41, 81, 245, 36, 0, 0, 0, 36, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255,
1420 161, 0, 0, 0, 0, 100, 105, 114, 101, 99, 116, 111, 114, 121, 92, 108, 105, 110,
1421 107, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 60, 0, 0, 0, 80, 0, 0, 0, 0, 0
1422 ] as &[u8],
1423 );
1424 }
1425
1426 #[test]
1427 fn write_mimetype_zip() {
1428 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1429 let options = FileOptions {
1430 compression_method: CompressionMethod::Stored,
1431 compression_level: None,
1432 last_modified_time: DateTime::default(),
1433 permissions: Some(33188),
1434 large_file: false,
1435 };
1436 writer.start_file("mimetype", options).unwrap();
1437 writer
1438 .write_all(b"application/vnd.oasis.opendocument.text")
1439 .unwrap();
1440 let result = writer.finish().unwrap();
1441
1442 assert_eq!(result.get_ref().len(), 153);
1443 let mut v = Vec::new();
1444 v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip"));
1445 assert_eq!(result.get_ref(), &v);
1446 }
1447
1448 #[test]
1449 fn path_to_string() {
1450 let mut path = std::path::PathBuf::new();
1451 #[cfg(windows)]
1452 path.push(r"C:\");
1453 #[cfg(unix)]
1454 path.push("/");
1455 path.push("windows");
1456 path.push("..");
1457 path.push(".");
1458 path.push("system32");
1459 let path_str = super::path_to_string(&path);
1460 assert_eq!(path_str, "windows/system32");
1461 }
1462}
1463
1464#[cfg(not(feature = "unreserved"))]
1465const EXTRA_FIELD_MAPPING: [u16; 49] = [
1466 0x0001, 0x0007, 0x0008, 0x0009, 0x000a, 0x000c, 0x000d, 0x000e, 0x000f, 0x0014, 0x0015, 0x0016,
1467 0x0017, 0x0018, 0x0019, 0x0020, 0x0021, 0x0022, 0x0023, 0x0065, 0x0066, 0x4690, 0x07c8, 0x2605,
1468 0x2705, 0x2805, 0x334d, 0x4341, 0x4453, 0x4704, 0x470f, 0x4b46, 0x4c41, 0x4d49, 0x4f4c, 0x5356,
1469 0x5455, 0x554e, 0x5855, 0x6375, 0x6542, 0x7075, 0x756e, 0x7855, 0xa11e, 0xa220, 0xfd4a, 0x9901,
1470 0x9902,
1471];