Skip to main content

zip/write/
options.rs

1//! Write Options
2
3use core::fmt::Debug;
4use std::marker::PhantomData;
5use std::sync::Arc;
6
7use crate::CompressionMethod;
8use crate::datetime::DateTime;
9use crate::extra_fields::CustomExtraField;
10use crate::format::ffi;
11use crate::format::system::System;
12use crate::result::{ZipResult, invalid};
13use crate::zipcrypto::ZipCryptoKeys;
14
15#[derive(Copy, Clone, Debug, Eq, PartialEq)]
16pub(crate) enum EncryptWith<'k> {
17    #[cfg(feature = "aes-crypto")]
18    Aes {
19        mode: crate::format::aes::AesMode,
20        vendor_version: crate::format::aes::AesVendorVersion,
21        // When the password is None, it means that we are reusing the previous encryption
22        password: Option<&'k [u8]>,
23        salt: Option<crate::aes::AesSalt>,
24    },
25    ZipCrypto(ZipCryptoKeys, PhantomData<&'k ()>),
26}
27
28#[cfg(feature = "_arbitrary")]
29impl<'a> arbitrary::Arbitrary<'a> for EncryptWith<'a> {
30    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
31        #[cfg(feature = "aes-crypto")]
32        if bool::arbitrary(u)? {
33            return Ok(EncryptWith::Aes {
34                mode: crate::format::aes::AesMode::arbitrary(u)?,
35                password: Some(u.arbitrary::<&[u8]>()?),
36                vendor_version: crate::format::aes::AesVendorVersion::Ae2,
37                salt: None, // We don't need to test with random salt. It's only for testing or reproducible zips
38            });
39        }
40
41        Ok(EncryptWith::ZipCrypto(
42            ZipCryptoKeys::arbitrary(u)?,
43            PhantomData,
44        ))
45    }
46}
47
48/// Metadata for a file to be written
49#[non_exhaustive]
50#[derive(Clone, Debug, Copy, Eq, PartialEq)]
51pub struct FileOptions<'k, 'n, T: sealed::FileOptionExtension> {
52    pub(crate) compression_method: CompressionMethod,
53    pub(crate) compression_level: Option<i64>,
54    pub(crate) external_attributes: Option<u32>,
55    pub(crate) last_modified_time: DateTime,
56    pub(crate) permissions: Option<u32>,
57    pub(crate) large_file: bool,
58    pub(crate) encrypt_with: Option<EncryptWith<'k>>,
59    pub(crate) extended_options: T,
60    pub(crate) alignment: u16,
61    #[cfg(feature = "deflate-zopfli")]
62    pub(super) zopfli_buffer_size: Option<usize>,
63    pub(crate) system: Option<System>,
64    pub(crate) name: Option<&'n [u8]>,
65}
66/// Simple File Options. Can be copied and good for simple writing zip files
67pub type SimpleFileOptions = FileOptions<'static, 'static, ()>;
68
69impl FileOptions<'static, 'static, ()> {
70    pub(crate) const DEFAULT_FILE_PERMISSION: u32 = 0o644 | ffi::S_IFREG;
71    pub(crate) const DEFAULT_DIR_PERMISSIONS: u32 = 0o755 | ffi::S_IFDIR; // rwxr-xr-x default for directories
72    pub(crate) const DEFAULT_SYMLINK_PERMISSIONS: u32 = 0o777 | ffi::S_IFLNK;
73}
74
75pub(crate) mod sealed {
76    use super::ExtendedFileOptions;
77    use crate::extra_fields::CustomExtraField;
78    use std::sync::Arc;
79
80    pub trait Sealed {}
81    /// File options Extensions
82    #[doc(hidden)]
83    pub trait FileOptionExtension: Default + Sealed {
84        /// Extra Data
85        fn extra_fields(&self) -> Option<&Arc<Vec<CustomExtraField>>>;
86        /// File Comment
87        fn file_comment(&self) -> Option<&str>;
88        /// Take File Comment (moves ownership)
89        fn take_file_comment(&mut self) -> Option<Box<str>>;
90    }
91    impl Sealed for () {}
92    impl FileOptionExtension for () {
93        fn extra_fields(&self) -> Option<&Arc<Vec<CustomExtraField>>> {
94            None
95        }
96        fn file_comment(&self) -> Option<&str> {
97            None
98        }
99        fn take_file_comment(&mut self) -> Option<Box<str>> {
100            None
101        }
102    }
103    impl Sealed for ExtendedFileOptions {}
104
105    impl FileOptionExtension for ExtendedFileOptions {
106        fn extra_fields(&self) -> Option<&Arc<Vec<CustomExtraField>>> {
107            Some(&self.extra_fields)
108        }
109        fn file_comment(&self) -> Option<&str> {
110            self.file_comment.as_ref().map(Box::as_ref)
111        }
112        fn take_file_comment(&mut self) -> Option<Box<str>> {
113            self.file_comment.take()
114        }
115    }
116}
117
118/// Adds Extra Data and Central Extra Data. It does not implement copy.
119pub type FullFileOptions<'k, 'n> = FileOptions<'k, 'n, ExtendedFileOptions>;
120/// The Extension for Extra Data and Central Extra Data
121#[cfg_attr(feature = "_arbitrary", derive(arbitrary::Arbitrary))]
122#[derive(Clone, Default, Eq, PartialEq)]
123pub struct ExtendedFileOptions {
124    pub(crate) extra_fields: Arc<Vec<CustomExtraField>>,
125    pub(crate) file_comment: Option<Box<str>>,
126}
127
128impl ExtendedFileOptions {
129    /// Adds an extra data field, unless we detect that it's invalid.
130    ///
131    /// # Parameters
132    ///
133    /// * `header_id` – The 2‑byte identifier of the ZIP extra field to add.
134    ///   This value determines the type/format of `data` and should either be
135    ///   one of the standard ZIP extra field IDs defined by the ZIP
136    ///   specification or an application‑specific (vendor) ID.
137    /// * `data` – The raw payload for the extra field, without the leading
138    ///   header ID or length; those are derived from `header_id` and
139    ///   `data.len()` and written automatically.
140    /// * `central_only` – Controls where the extra field is stored:
141    ///   * When `true`, the field is appended only to the central directory
142    ///     extra data (`central_extra_data`), and the corresponding local file
143    ///     header is left unchanged.
144    ///   * When `false`, the field is appended to the local file header extra
145    ///     data (`extra_data`) and may also be reflected in the central
146    ///     directory, depending on how the ZIP is written.
147    ///
148    /// The combined size of all extra data (local + central) must not exceed
149    /// `u16::MAX`. If adding this field would exceed that limit or produce an
150    /// invalid extra data structure, an error is returned and no data is
151    /// added.
152    #[deprecated = "use add_extra_field()"]
153    pub fn add_extra_data<D: AsRef<[u8]>>(
154        &mut self,
155        header_id: u16,
156        data: D,
157        central_only: bool,
158    ) -> ZipResult<()> {
159        self.add_extra_field(header_id, data, central_only)
160    }
161    /// Adds an extra field, unless we detect that it's invalid.
162    ///
163    /// # Parameters
164    ///
165    /// * `header_id` – The 2‑byte identifier of the ZIP extra field to add.
166    ///   This value determines the type/format of `data` and should either be
167    ///   one of the standard ZIP extra field IDs defined by the ZIP
168    ///   specification or an application‑specific (vendor) ID.
169    /// * `data` – The raw payload for the extra field, without the leading
170    ///   header ID or length; those are derived from `header_id` and
171    ///   `data.len()` and written automatically.
172    /// * `central_only` – Controls where the extra field is stored:
173    ///   * When `true`, the field is appended only to the central directory
174    ///     extra data (`central_extra_data`), and the corresponding local file
175    ///     header is left unchanged.
176    ///   * When `false`, the field is appended to the local file header extra
177    ///     data (`extra_data`) and may also be reflected in the central
178    ///     directory, depending on how the ZIP is written.
179    ///
180    /// The combined size of all extra data (local + central) must not exceed
181    /// `u16::MAX`. If adding this field would exceed that limit or produce an
182    /// invalid extra data structure, an error is returned and no data is
183    /// added.
184    pub fn add_extra_field<D: AsRef<[u8]>>(
185        &mut self,
186        header_id: u16,
187        data: D,
188        central_only: bool,
189    ) -> ZipResult<()> {
190        let data = data.as_ref();
191        let len = data.len() + 4;
192        let extra_fields_len: usize = self.extra_fields.iter().map(|x| x.full_size(false)).sum();
193        if extra_fields_len + len > u16::MAX as usize {
194            Err(invalid!("Extra data field would be longer than allowed"))
195        } else {
196            Arc::make_mut(&mut self.extra_fields).push(CustomExtraField::new(
197                central_only,
198                header_id,
199                data.to_vec().into_boxed_slice(),
200            ));
201            Ok(())
202        }
203    }
204}
205
206impl Debug for ExtendedFileOptions {
207    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
208        f.debug_struct("ExtendedFileOptions")
209            .field("extra_fields", &self.extra_fields)
210            .field("file_comment", &self.file_comment)
211            .finish()
212    }
213}
214
215#[cfg(feature = "_arbitrary")]
216impl<'k, 'n, 'a: 'k + 'n> arbitrary::Arbitrary<'a> for FileOptions<'k, 'n, ExtendedFileOptions> {
217    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
218        use crate::write::ZipWriter;
219
220        let mut options = FullFileOptions {
221            compression_method: CompressionMethod::arbitrary(u)?,
222            compression_level: if bool::arbitrary(u)? {
223                Some(u.int_in_range(0..=24)?)
224            } else {
225                None
226            },
227            last_modified_time: DateTime::arbitrary(u)?,
228            permissions: Option::<u32>::arbitrary(u)?,
229            large_file: bool::arbitrary(u)?,
230            encrypt_with: Option::<EncryptWith<'_>>::arbitrary(u)?,
231            alignment: u16::arbitrary(u)?,
232            #[cfg(feature = "deflate-zopfli")]
233            zopfli_buffer_size: None,
234            ..Default::default()
235        };
236        #[cfg(feature = "deflate-zopfli")]
237        if options.compression_method == CompressionMethod::Deflated && bool::arbitrary(u)? {
238            options.zopfli_buffer_size =
239                Some(if bool::arbitrary(u)? { 2 } else { 3 } << u.int_in_range(8..=20)?);
240        }
241        u.arbitrary_loop(Some(0), Some(10), |u| {
242            options
243                .add_extra_field(
244                    u.int_in_range(2..=u16::MAX)?,
245                    Box::<[u8]>::arbitrary(u)?,
246                    bool::arbitrary(u)?,
247                )
248                .map_err(|_| arbitrary::Error::IncorrectFormat)?;
249            Ok(core::ops::ControlFlow::Continue(()))
250        })?;
251        let len = u.arbitrary_len::<u8>()?;
252        options.name = Some(u.bytes(len)?);
253        ZipWriter::new(std::io::Cursor::new(Vec::new()))
254            .start_file("", options.clone())
255            .map_err(|_| arbitrary::Error::IncorrectFormat)?;
256        Ok(options)
257    }
258}
259
260impl<'k, 'n, T: sealed::FileOptionExtension> FileOptions<'k, 'n, T> {
261    pub(crate) fn normalize(&mut self) {
262        if !self.last_modified_time.is_valid() {
263            self.last_modified_time = FileOptions::<T>::default().last_modified_time;
264        }
265        // The permissions set by the user
266        if let Some(perms) = &mut self.permissions
267            && (*perms & !0o777 == 0)
268        {
269            *perms |= ffi::S_IFREG;
270        }
271    }
272
273    /// Indicates whether this file will be encrypted (whether with AES or `ZipCrypto`).
274    pub const fn has_encryption(&self) -> bool {
275        self.encrypt_with.is_some()
276    }
277
278    /// Set the compression method for the new file
279    ///
280    /// The default is [`CompressionMethod::Deflated`] if it is enabled. If not,
281    /// [`CompressionMethod::Bzip2`] is the default if it is enabled. If neither `bzip2` nor `deflate`
282    /// is enabled, [`CompressionMethod::Stored`] becomes the default and files are written uncompressed.
283    #[must_use]
284    pub const fn compression_method(mut self, method: CompressionMethod) -> Self {
285        self.compression_method = method;
286        self
287    }
288
289    /// Set the `system` field for the new file
290    ///
291    /// If not set, the `zip` crate will use the current system
292    #[must_use]
293    pub const fn system(mut self, system: System) -> Self {
294        self.system = Some(system);
295        self
296    }
297
298    /// Set the compression level for the new file
299    ///
300    /// `None` value specifies default compression level.
301    ///
302    /// Range of values depends on compression method:
303    /// * `Deflated`: 10 - 264 for Zopfli, 0 - 9 for other encoders. Default is 24 if Zopfli is the
304    ///   only encoder, or 6 otherwise.
305    /// * `Bzip2`: 0 - 9. Default is 6
306    /// * `Zstd`: -7 - 22, with zero being mapped to default level. Default is 3
307    /// * others: only `None` is allowed
308    #[must_use]
309    pub const fn compression_level(mut self, level: Option<i64>) -> Self {
310        self.compression_level = level;
311        self
312    }
313
314    /// Set the last modified time
315    ///
316    /// The default is the current timestamp if the 'time' feature is enabled, and 1980-01-01
317    /// otherwise
318    #[must_use]
319    pub const fn last_modified_time(mut self, mod_time: DateTime) -> Self {
320        self.last_modified_time = mod_time;
321        self
322    }
323
324    /// Set the permissions for the new file.
325    ///
326    /// The format is represented with unix-style permissions.
327    /// The default is `0o644`, which represents `rw-r--r--` for files,
328    /// and `0o755`, which represents `rwxr-xr-x` for directories.
329    ///
330    /// This method only preserves the file permissions bits (via a `& 0o777`) and discards
331    /// higher file mode bits. So it cannot be used to denote an entry as a directory,
332    /// symlink, or other special file type.
333    #[must_use]
334    pub const fn unix_permissions(mut self, mode: u32) -> Self {
335        self.permissions = Some(mode & 0o777);
336        self
337    }
338
339    /// Set the external attributes for the file.
340    ///
341    /// If you use both [`Self::unix_permissions`] and [`Self::external_attributes`], only external
342    /// attributes are going to be used
343    #[must_use]
344    pub const fn external_attributes(mut self, external_perms: u32) -> Self {
345        self.external_attributes = Some(external_perms);
346        self
347    }
348
349    /// Set whether the new file's compressed and uncompressed size is less than 4 GiB.
350    ///
351    /// If set to `false` and the file exceeds the limit, an I/O error is thrown and the file is
352    /// aborted. If set to `true`, readers will require ZIP64 support and if the file does not
353    /// exceed the limit, 20 B are wasted. The default is `false`.
354    #[must_use]
355    pub const fn large_file(mut self, large: bool) -> Self {
356        self.large_file = large;
357        self
358    }
359
360    pub(crate) fn with_deprecated_encryption(self, password: &'k [u8]) -> FileOptions<'k, 'n, T> {
361        FileOptions {
362            encrypt_with: Some(EncryptWith::ZipCrypto(
363                ZipCryptoKeys::derive(password),
364                PhantomData,
365            )),
366            ..self
367        }
368    }
369
370    /// Set the AES encryption parameters.
371    /// The `salt` must be at least 8 bytes long for AES-128, and at least 16 bytes long for AES-256.
372    /// This method is not recommended, since having a fixed salt is not secure.
373    /// Consider using `with_aes_encryption` instead, which uses a random salt and is more secure.
374    #[cfg(feature = "aes-crypto")]
375    pub fn with_aes_encryption_and_salt(
376        self,
377        password: &'k [u8],
378        salt: crate::aes::AesSalt,
379    ) -> FileOptions<'k, 'n, T> {
380        FileOptions {
381            encrypt_with: Some(EncryptWith::Aes {
382                mode: salt.mode(),
383                password: Some(password),
384                vendor_version: crate::format::aes::AesVendorVersion::Ae2,
385                salt: Some(salt),
386            }),
387            ..self
388        }
389    }
390
391    /// Set the AES encryption parameters.
392    #[cfg(feature = "aes-crypto")]
393    pub fn with_aes_encryption(
394        self,
395        mode: crate::AesMode,
396        password: &'k str,
397    ) -> FileOptions<'k, 'n, T> {
398        self.with_aes_encryption_bytes(mode, password.as_bytes())
399    }
400
401    /// Set the AES encryption parameters.
402    #[cfg(feature = "aes-crypto")]
403    pub fn with_aes_encryption_bytes(
404        self,
405        mode: crate::AesMode,
406        password: &'k [u8],
407    ) -> FileOptions<'k, 'n, T> {
408        FileOptions {
409            encrypt_with: Some(EncryptWith::Aes {
410                mode,
411                password: Some(password),
412                vendor_version: crate::format::aes::AesVendorVersion::Ae2,
413                salt: None,
414            }),
415            ..self
416        }
417    }
418
419    /// Sets the size of the buffer used to hold the next block that Zopfli will compress. The
420    /// larger the buffer, the more effective the compression, but the more memory is required.
421    /// A value of `None` indicates no buffer, which is recommended only when all non-empty writes
422    /// are larger than about 32 KiB.
423    #[must_use]
424    #[cfg(feature = "deflate-zopfli")]
425    pub const fn with_zopfli_buffer(mut self, size: Option<usize>) -> Self {
426        self.zopfli_buffer_size = size;
427        self
428    }
429
430    /// Returns the compression level currently set.
431    pub const fn get_compression_level(&self) -> Option<i64> {
432        self.compression_level
433    }
434    /// Sets the alignment to the given number of bytes.
435    #[must_use]
436    pub const fn with_alignment(mut self, alignment: u16) -> Self {
437        self.alignment = alignment;
438        self
439    }
440}
441impl FileOptions<'_, '_, ExtendedFileOptions> {
442    /// Set the file comment.
443    #[must_use]
444    pub fn with_file_comment<S: Into<Box<str>>>(mut self, comment: S) -> Self {
445        self.extended_options.file_comment = Some(comment.into());
446        self
447    }
448
449    /// Adds an extra data field.
450    #[deprecated = "use add_extra_field()"]
451    pub fn add_extra_data<D: AsRef<[u8]>>(
452        &mut self,
453        header_id: u16,
454        data: D,
455        central_only: bool,
456    ) -> ZipResult<()> {
457        self.add_extra_field(header_id, data, central_only)
458    }
459
460    /// Adds an extra field.
461    pub fn add_extra_field<D: AsRef<[u8]>>(
462        &mut self,
463        header_id: u16,
464        data: D,
465        central_only: bool,
466    ) -> ZipResult<()> {
467        self.extended_options
468            .add_extra_field(header_id, data, central_only)
469    }
470
471    /// Removes the extra fields.
472    #[must_use]
473    #[deprecated = "use clear_extra_fields"]
474    pub fn clear_extra_data(self) -> Self {
475        self.clear_extra_fields()
476    }
477
478    /// Removes the extra fields.
479    #[must_use]
480    pub fn clear_extra_fields(mut self) -> Self {
481        if !self.extended_options.extra_fields.is_empty() {
482            self.extended_options.extra_fields = Arc::new(vec![]);
483        }
484        self
485    }
486}
487impl FileOptions<'static, 'static, ()> {
488    /// Constructs a const `FileOptions` object.
489    ///
490    /// Note: This value is different than the return value of [`FileOptions::default()`]:
491    ///
492    /// - The `last_modified_time` is [`DateTime::DEFAULT`]. This corresponds to 1980-01-01 00:00:00
493    pub const DEFAULT: Self = Self {
494        compression_method: CompressionMethod::DEFAULT,
495        compression_level: None,
496        last_modified_time: DateTime::DEFAULT,
497        large_file: false,
498        permissions: None,
499        encrypt_with: None,
500        extended_options: (),
501        alignment: 1,
502        #[cfg(feature = "deflate-zopfli")]
503        zopfli_buffer_size: Some(1 << 15),
504        system: None,
505        name: None,
506        external_attributes: None,
507    };
508}
509
510impl<'k, 'n> FileOptions<'k, 'n, ()> {
511    /// Convert to `FullFileOptions`.
512    #[must_use]
513    pub fn into_full_options(self) -> FullFileOptions<'k, 'n> {
514        FileOptions {
515            compression_method: self.compression_method,
516            compression_level: self.compression_level,
517            last_modified_time: self.last_modified_time,
518            permissions: self.permissions,
519            large_file: self.large_file,
520            encrypt_with: self.encrypt_with,
521            extended_options: ExtendedFileOptions::default(),
522            alignment: self.alignment,
523            #[cfg(feature = "deflate-zopfli")]
524            zopfli_buffer_size: self.zopfli_buffer_size,
525            system: self.system,
526            name: self.name,
527            external_attributes: self.external_attributes,
528        }
529    }
530}
531
532impl<T: sealed::FileOptionExtension> Default for FileOptions<'_, '_, T> {
533    /// Construct a new `FileOptions` object
534    fn default() -> Self {
535        Self {
536            compression_method: CompressionMethod::default(),
537            compression_level: None,
538            last_modified_time: DateTime::default_for_write(),
539            permissions: None,
540            large_file: false,
541            encrypt_with: None,
542            extended_options: T::default(),
543            alignment: 1,
544            #[cfg(feature = "deflate-zopfli")]
545            zopfli_buffer_size: Some(1 << 15),
546            system: None,
547            name: None,
548            external_attributes: None,
549        }
550    }
551}