binstall_zip/
write.rs

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::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}
45// Put the struct declaration in a private module to convince rustdoc to display ZipWriter nicely
46pub(crate) mod zip_writer {
47    use super::*;
48    /// ZIP archive generator
49    ///
50    /// Handles the bookkeeping involved in building an archive, and provides an
51    /// API to edit its contents.
52    ///
53    /// ```
54    /// # fn doit() -> zip::result::ZipResult<()>
55    /// # {
56    /// # use zip::ZipWriter;
57    /// use std::io::Write;
58    /// use zip::write::FileOptions;
59    ///
60    /// // We use a buffer here, though you'd normally use a `File`
61    /// let mut buf = [0; 65536];
62    /// let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf[..]));
63    ///
64    /// let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
65    /// zip.start_file("hello_world.txt", options)?;
66    /// zip.write(b"Hello, World!")?;
67    ///
68    /// // Apply the changes you've made.
69    /// // Dropping the `ZipWriter` will have the same effect, but may silently fail
70    /// zip.finish()?;
71    ///
72    /// # Ok(())
73    /// # }
74    /// # doit().unwrap();
75    /// ```
76    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/// Metadata for a file to be written
103#[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    /// Construct a new FileOptions object
114    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    /// Set the compression method for the new file
139    ///
140    /// The default is `CompressionMethod::Deflated`. If the deflate compression feature is
141    /// disabled, `CompressionMethod::Stored` becomes the default.
142    #[must_use]
143    pub fn compression_method(mut self, method: CompressionMethod) -> FileOptions {
144        self.compression_method = method;
145        self
146    }
147
148    /// Set the compression level for the new file
149    ///
150    /// `None` value specifies default compression level.
151    ///
152    /// Range of values depends on compression method:
153    /// * `Deflated`: 0 - 9. Default is 6
154    /// * `Bzip2`: 0 - 9. Default is 6
155    /// * `Zstd`: -7 - 22, with zero being mapped to default level. Default is 3
156    /// * others: only `None` is allowed
157    #[must_use]
158    pub fn compression_level(mut self, level: Option<i32>) -> FileOptions {
159        self.compression_level = level;
160        self
161    }
162
163    /// Set the last modified time
164    ///
165    /// The default is the current timestamp if the 'time' feature is enabled, and 1980-01-01
166    /// otherwise
167    #[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    /// Set the permissions for the new file.
174    ///
175    /// The format is represented with unix-style permissions.
176    /// The default is `0o644`, which represents `rw-r--r--` for files,
177    /// and `0o755`, which represents `rwxr-xr-x` for directories.
178    ///
179    /// This method only preserves the file permissions bits (via a `& 0o777`) and discards
180    /// higher file mode bits. So it cannot be used to denote an entry as a directory,
181    /// symlink, or other special file type.
182    #[must_use]
183    pub fn unix_permissions(mut self, mode: u32) -> FileOptions {
184        self.permissions = Some(mode & 0o777);
185        self
186    }
187
188    /// Set whether the new file's compressed and uncompressed size is less than 4 GiB.
189    ///
190    /// If set to `false` and the file exceeds the limit, an I/O error is thrown. If set to `true`,
191    /// readers will require ZIP64 support and if the file does not exceed the limit, 20 B are
192    /// wasted. The default is `false`.
193    #[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    /// Initializes the archive from an existing ZIP archive, making it ready for append.
262    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)); // seek directory_start to overwrite it
288
289        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, // avoid recomputing the last file's header
298        })
299    }
300}
301
302impl<W: Write + io::Seek> ZipWriter<W> {
303    /// Initializes the archive.
304    ///
305    /// Before writing to this object, the [`ZipWriter::start_file`] function should be called.
306    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    /// Set ZIP archive comment.
320    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    /// Set ZIP archive comment.
328    ///
329    /// This sets the raw bytes of the comment. The comment
330    /// is typically expected to be encoded in UTF-8
331    pub fn set_raw_comment(&mut self, comment: Vec<u8>) {
332        self.comment = comment;
333    }
334
335    /// Start a new file for with the requested options.
336    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(), // Never used for saving
371                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            // Implicitly calling [`ZipWriter::end_extra_data`] for empty files.
398            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    /// Create a file in the archive and start writing its' contents.
424    ///
425    /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`]
426    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    /// Starts a file, taking a Path as argument.
442    ///
443    /// This function ensures that the '/' path separator is used. It also ignores all non 'Normal'
444    /// Components, such as a starting '/' or '..' and '.'.
445    #[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    /// Create an aligned file in the archive and start writing its' contents.
458    ///
459    /// Returns the number of padding bytes required to align the file.
460    ///
461    /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`]
462    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)?; // 0x617a
477            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    /// Create a file in the archive and start writing its extra data first.
487    ///
488    /// Finish writing extra data and start writing file data with [`ZipWriter::end_extra_data`].
489    /// Optionally, distinguish local from central extra data with
490    /// [`ZipWriter::end_local_start_central_extra_data`].
491    ///
492    /// Returns the preliminary starting offset of the file data without any extra data allowing to
493    /// align the file data by calculating a pad length to be prepended as part of the extra data.
494    ///
495    /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`]
496    ///
497    /// ```
498    /// use byteorder::{LittleEndian, WriteBytesExt};
499    /// use zip::{ZipArchive, ZipWriter, result::ZipResult};
500    /// use zip::{write::FileOptions, CompressionMethod};
501    /// use std::io::{Write, Cursor};
502    ///
503    /// # fn main() -> ZipResult<()> {
504    /// let mut archive = Cursor::new(Vec::new());
505    ///
506    /// {
507    ///     let mut zip = ZipWriter::new(&mut archive);
508    ///     let options = FileOptions::default()
509    ///         .compression_method(CompressionMethod::Stored);
510    ///
511    ///     zip.start_file_with_extra_data("identical_extra_data.txt", options)?;
512    ///     let extra_data = b"local and central extra data";
513    ///     zip.write_u16::<LittleEndian>(0xbeef)?;
514    ///     zip.write_u16::<LittleEndian>(extra_data.len() as u16)?;
515    ///     zip.write_all(extra_data)?;
516    ///     zip.end_extra_data()?;
517    ///     zip.write_all(b"file data")?;
518    ///
519    ///     let data_start = zip.start_file_with_extra_data("different_extra_data.txt", options)?;
520    ///     let extra_data = b"local extra data";
521    ///     zip.write_u16::<LittleEndian>(0xbeef)?;
522    ///     zip.write_u16::<LittleEndian>(extra_data.len() as u16)?;
523    ///     zip.write_all(extra_data)?;
524    ///     let data_start = data_start as usize + 4 + extra_data.len() + 4;
525    ///     let align = 64;
526    ///     let pad_length = (align - data_start % align) % align;
527    ///     assert_eq!(pad_length, 19);
528    ///     zip.write_u16::<LittleEndian>(0xdead)?;
529    ///     zip.write_u16::<LittleEndian>(pad_length as u16)?;
530    ///     zip.write_all(&vec![0; pad_length])?;
531    ///     let data_start = zip.end_local_start_central_extra_data()?;
532    ///     assert_eq!(data_start as usize % align, 0);
533    ///     let extra_data = b"central extra data";
534    ///     zip.write_u16::<LittleEndian>(0xbeef)?;
535    ///     zip.write_u16::<LittleEndian>(extra_data.len() as u16)?;
536    ///     zip.write_all(extra_data)?;
537    ///     zip.end_extra_data()?;
538    ///     zip.write_all(b"file data")?;
539    ///
540    ///     zip.finish()?;
541    /// }
542    ///
543    /// let mut zip = ZipArchive::new(archive)?;
544    /// assert_eq!(&zip.by_index(0)?.extra_data()[4..], b"local and central extra data");
545    /// assert_eq!(&zip.by_index(1)?.extra_data()[4..], b"central extra data");
546    /// # Ok(())
547    /// # }
548    /// ```
549    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    /// End local and start central extra data. Requires [`ZipWriter::start_file_with_extra_data`].
568    ///
569    /// Returns the final starting offset of the file data.
570    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    /// End extra data and start file data. Requires [`ZipWriter::start_file_with_extra_data`].
579    ///
580    /// Returns the final starting offset of the file data.
581    pub fn end_extra_data(&mut self) -> ZipResult<u64> {
582        // Require `start_file_with_extra_data()`. Ensures `file` is some.
583        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            // Append extra data to local file header and keep it for central file header.
599            writer.write_all(&file.extra_field)?;
600
601            // Update final `data_start`.
602            let header_end = *data_start + file.extra_field.len() as u64;
603            self.stats.start = header_end;
604            *data_start = header_end;
605
606            // Update extra field length in local file header.
607            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    /// Add a new file using the already compressed data from a ZIP file being read and renames it, this
623    /// allows faster copies of the `ZipFile` since there is no need to decompress and compress it again.
624    /// Any `ZipFile` metadata is copied and not checked, for example the file CRC.
625
626    /// ```no_run
627    /// use std::fs::File;
628    /// use std::io::{Read, Seek, Write};
629    /// use zip::{ZipArchive, ZipWriter};
630    ///
631    /// fn copy_rename<R, W>(
632    ///     src: &mut ZipArchive<R>,
633    ///     dst: &mut ZipWriter<W>,
634    /// ) -> zip::result::ZipResult<()>
635    /// where
636    ///     R: Read + Seek,
637    ///     W: Write + Seek,
638    /// {
639    ///     // Retrieve file entry by name
640    ///     let file = src.by_name("src_file.txt")?;
641    ///
642    ///     // Copy and rename the previously obtained file entry to the destination zip archive
643    ///     dst.raw_copy_file_rename(file, "new_name.txt")?;
644    ///
645    ///     Ok(())
646    /// }
647    /// ```
648    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    /// Add a new file using the already compressed data from a ZIP file being read, this allows faster
676    /// copies of the `ZipFile` since there is no need to decompress and compress it again. Any `ZipFile`
677    /// metadata is copied and not checked, for example the file CRC.
678    ///
679    /// ```no_run
680    /// use std::fs::File;
681    /// use std::io::{Read, Seek, Write};
682    /// use zip::{ZipArchive, ZipWriter};
683    ///
684    /// fn copy<R, W>(src: &mut ZipArchive<R>, dst: &mut ZipWriter<W>) -> zip::result::ZipResult<()>
685    /// where
686    ///     R: Read + Seek,
687    ///     W: Write + Seek,
688    /// {
689    ///     // Retrieve file entry by name
690    ///     let file = src.by_name("src_file.txt")?;
691    ///
692    ///     // Copy the previously obtained file entry to the destination zip archive
693    ///     dst.raw_copy_file(file)?;
694    ///
695    ///     Ok(())
696    /// }
697    /// ```
698    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    /// Add a directory entry.
704    ///
705    /// You can't write data to the file afterwards.
706    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        // Append a slash to the filename if it does not end with it.
718        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    /// Add a directory entry, taking a Path as argument.
729    ///
730    /// This function ensures that the '/' path separator is used. It also ignores all non 'Normal'
731    /// Components, such as a starting '/' or '..' and '.'.
732    #[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    /// Finish the last file and write all other zip-structures
745    ///
746    /// This will return the writer, but one should normally not append any data to the end of the file.
747    /// Note that the zipfile will also be finished on drop.
748    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    /// Add a symlink entry.
755    ///
756    /// The zip archive will contain an entry for path `name` which is a symlink to `target`.
757    ///
758    /// No validation or normalization of the paths is performed. For best results,
759    /// callers should normalize `\` to `/` and ensure symlinks are relative to other
760    /// paths within the zip archive.
761    ///
762    /// WARNING: not all zip implementations preserve symlinks on extract. Some zip
763    /// implementations may materialize a symlink as a regular file, possibly with the
764    /// content incorrectly set to the symlink target. For maximum portability, consider
765    /// storing a regular file instead.
766    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        // The symlink target is stored as file content. And compressing the target path
781        // likely wastes space. So always store.
782        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    // local file header signature
1056    writer.write_u32::<LittleEndian>(spec::LOCAL_FILE_HEADER_SIGNATURE)?;
1057    // version needed to extract
1058    writer.write_u16::<LittleEndian>(file.version_needed())?;
1059    // general purpose bit flag
1060    let flag = if !file.file_name.is_ascii() {
1061        1u16 << 11
1062    } else {
1063        0
1064    };
1065    writer.write_u16::<LittleEndian>(flag)?;
1066    // Compression method
1067    #[allow(deprecated)]
1068    writer.write_u16::<LittleEndian>(file.compression_method.to_u16())?;
1069    // last mod file time and last mod file date
1070    writer.write_u16::<LittleEndian>(file.last_modified_time.timepart())?;
1071    writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?;
1072    // crc-32
1073    writer.write_u32::<LittleEndian>(file.crc32)?;
1074    // compressed size and uncompressed size
1075    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    // file name length
1083    writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
1084    // extra field length
1085    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    // file name
1088    writer.write_all(file.file_name.as_bytes())?;
1089    // zip64 extra field
1090    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        // check compressed size as well as it can also be slightly larger than uncompressed size
1108        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        // uncompressed size is already checked on write to catch it as soon as possible
1116        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    // buffer zip64 extra field to determine its variable length
1123    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    // central file header signature
1128    writer.write_u32::<LittleEndian>(spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE)?;
1129    // version made by
1130    let version_made_by = (file.system as u16) << 8 | (file.version_made_by as u16);
1131    writer.write_u16::<LittleEndian>(version_made_by)?;
1132    // version needed to extract
1133    writer.write_u16::<LittleEndian>(file.version_needed())?;
1134    // general puprose bit flag
1135    let flag = if !file.file_name.is_ascii() {
1136        1u16 << 11
1137    } else {
1138        0
1139    };
1140    writer.write_u16::<LittleEndian>(flag)?;
1141    // compression method
1142    #[allow(deprecated)]
1143    writer.write_u16::<LittleEndian>(file.compression_method.to_u16())?;
1144    // last mod file time + date
1145    writer.write_u16::<LittleEndian>(file.last_modified_time.timepart())?;
1146    writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?;
1147    // crc-32
1148    writer.write_u32::<LittleEndian>(file.crc32)?;
1149    // compressed size
1150    writer.write_u32::<LittleEndian>(file.compressed_size.min(spec::ZIP64_BYTES_THR) as u32)?;
1151    // uncompressed size
1152    writer.write_u32::<LittleEndian>(file.uncompressed_size.min(spec::ZIP64_BYTES_THR) as u32)?;
1153    // file name length
1154    writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
1155    // extra field length
1156    writer.write_u16::<LittleEndian>(zip64_extra_field_length + file.extra_field.len() as u16)?;
1157    // file comment length
1158    writer.write_u16::<LittleEndian>(0)?;
1159    // disk number start
1160    writer.write_u16::<LittleEndian>(0)?;
1161    // internal file attribytes
1162    writer.write_u16::<LittleEndian>(0)?;
1163    // external file attributes
1164    writer.write_u32::<LittleEndian>(file.external_attributes)?;
1165    // relative offset of local header
1166    writer.write_u32::<LittleEndian>(file.header_start.min(spec::ZIP64_BYTES_THR) as u32)?;
1167    // file name
1168    writer.write_all(file.file_name.as_bytes())?;
1169    // zip64 extra field
1170    writer.write_all(&zip64_extra_field[..zip64_extra_field_length as usize])?;
1171    // extra field
1172    writer.write_all(&file.extra_field)?;
1173    // file comment
1174    // <none>
1175
1176    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    // This entry in the Local header MUST include BOTH original
1236    // and compressed file size fields.
1237    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    // Excluded fields:
1242    // u32: disk start number
1243    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    // Excluded fields:
1255    // u32: disk start number
1256    Ok(())
1257}
1258
1259fn write_central_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<u16> {
1260    // The order of the fields in the zip64 extended
1261    // information record is fixed, but the fields MUST
1262    // only appear if the corresponding Local or Central
1263    // directory record field is set to 0xFFFF or 0xFFFFFFFF.
1264    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        // Excluded fields:
1292        // u32: disk start number
1293    }
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        // unix_permissions() throws away upper bits.
1333        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];