summaryrefslogtreecommitdiffstats
path: root/third_party/rust/zip/src/write.rs
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/rust/zip/src/write.rs')
-rw-r--r--third_party/rust/zip/src/write.rs1471
1 files changed, 1471 insertions, 0 deletions
diff --git a/third_party/rust/zip/src/write.rs b/third_party/rust/zip/src/write.rs
new file mode 100644
index 0000000000..61ce378c0c
--- /dev/null
+++ b/third_party/rust/zip/src/write.rs
@@ -0,0 +1,1471 @@
+//! Types for creating ZIP archives
+
+use crate::compression::CompressionMethod;
+use crate::read::{central_header_to_zip_file, ZipArchive, ZipFile};
+use crate::result::{ZipError, ZipResult};
+use crate::spec;
+use crate::types::{AtomicU64, DateTime, System, ZipFileData, DEFAULT_VERSION};
+use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
+use crc32fast::Hasher;
+use std::default::Default;
+use std::io;
+use std::io::prelude::*;
+use std::mem;
+
+#[cfg(any(
+ feature = "deflate",
+ feature = "deflate-miniz",
+ feature = "deflate-zlib"
+))]
+use flate2::write::DeflateEncoder;
+
+#[cfg(feature = "bzip2")]
+use bzip2::write::BzEncoder;
+
+#[cfg(feature = "time")]
+use time::OffsetDateTime;
+
+#[cfg(feature = "zstd")]
+use zstd::stream::write::Encoder as ZstdEncoder;
+
+enum GenericZipWriter<W: Write + io::Seek> {
+ Closed,
+ Storer(W),
+ #[cfg(any(
+ feature = "deflate",
+ feature = "deflate-miniz",
+ feature = "deflate-zlib"
+ ))]
+ Deflater(DeflateEncoder<W>),
+ #[cfg(feature = "bzip2")]
+ Bzip2(BzEncoder<W>),
+ #[cfg(feature = "zstd")]
+ Zstd(ZstdEncoder<'static, W>),
+}
+// Put the struct declaration in a private module to convince rustdoc to display ZipWriter nicely
+pub(crate) mod zip_writer {
+ use super::*;
+ /// ZIP archive generator
+ ///
+ /// Handles the bookkeeping involved in building an archive, and provides an
+ /// API to edit its contents.
+ ///
+ /// ```
+ /// # fn doit() -> zip::result::ZipResult<()>
+ /// # {
+ /// # use zip::ZipWriter;
+ /// use std::io::Write;
+ /// use zip::write::FileOptions;
+ ///
+ /// // We use a buffer here, though you'd normally use a `File`
+ /// let mut buf = [0; 65536];
+ /// let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf[..]));
+ ///
+ /// let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
+ /// zip.start_file("hello_world.txt", options)?;
+ /// zip.write(b"Hello, World!")?;
+ ///
+ /// // Apply the changes you've made.
+ /// // Dropping the `ZipWriter` will have the same effect, but may silently fail
+ /// zip.finish()?;
+ ///
+ /// # Ok(())
+ /// # }
+ /// # doit().unwrap();
+ /// ```
+ pub struct ZipWriter<W: Write + io::Seek> {
+ pub(super) inner: GenericZipWriter<W>,
+ pub(super) files: Vec<ZipFileData>,
+ pub(super) stats: ZipWriterStats,
+ pub(super) writing_to_file: bool,
+ pub(super) writing_to_extra_field: bool,
+ pub(super) writing_to_central_extra_field_only: bool,
+ pub(super) writing_raw: bool,
+ pub(super) comment: Vec<u8>,
+ }
+}
+pub use zip_writer::ZipWriter;
+
+#[derive(Default)]
+struct ZipWriterStats {
+ hasher: Hasher,
+ start: u64,
+ bytes_written: u64,
+}
+
+struct ZipRawValues {
+ crc32: u32,
+ compressed_size: u64,
+ uncompressed_size: u64,
+}
+
+/// Metadata for a file to be written
+#[derive(Copy, Clone)]
+pub struct FileOptions {
+ compression_method: CompressionMethod,
+ compression_level: Option<i32>,
+ last_modified_time: DateTime,
+ permissions: Option<u32>,
+ large_file: bool,
+}
+
+impl FileOptions {
+ /// Construct a new FileOptions object
+ pub fn default() -> FileOptions {
+ FileOptions {
+ #[cfg(any(
+ feature = "deflate",
+ feature = "deflate-miniz",
+ feature = "deflate-zlib"
+ ))]
+ compression_method: CompressionMethod::Deflated,
+ #[cfg(not(any(
+ feature = "deflate",
+ feature = "deflate-miniz",
+ feature = "deflate-zlib"
+ )))]
+ compression_method: CompressionMethod::Stored,
+ compression_level: None,
+ #[cfg(feature = "time")]
+ last_modified_time: DateTime::from_time(OffsetDateTime::now_utc()).unwrap_or_default(),
+ #[cfg(not(feature = "time"))]
+ last_modified_time: DateTime::default(),
+ permissions: None,
+ large_file: false,
+ }
+ }
+
+ /// Set the compression method for the new file
+ ///
+ /// The default is `CompressionMethod::Deflated`. If the deflate compression feature is
+ /// disabled, `CompressionMethod::Stored` becomes the default.
+ #[must_use]
+ pub fn compression_method(mut self, method: CompressionMethod) -> FileOptions {
+ self.compression_method = method;
+ self
+ }
+
+ /// Set the compression level for the new file
+ ///
+ /// `None` value specifies default compression level.
+ ///
+ /// Range of values depends on compression method:
+ /// * `Deflated`: 0 - 9. Default is 6
+ /// * `Bzip2`: 0 - 9. Default is 6
+ /// * `Zstd`: -7 - 22, with zero being mapped to default level. Default is 3
+ /// * others: only `None` is allowed
+ #[must_use]
+ pub fn compression_level(mut self, level: Option<i32>) -> FileOptions {
+ self.compression_level = level;
+ self
+ }
+
+ /// Set the last modified time
+ ///
+ /// The default is the current timestamp if the 'time' feature is enabled, and 1980-01-01
+ /// otherwise
+ #[must_use]
+ pub fn last_modified_time(mut self, mod_time: DateTime) -> FileOptions {
+ self.last_modified_time = mod_time;
+ self
+ }
+
+ /// Set the permissions for the new file.
+ ///
+ /// The format is represented with unix-style permissions.
+ /// The default is `0o644`, which represents `rw-r--r--` for files,
+ /// and `0o755`, which represents `rwxr-xr-x` for directories.
+ ///
+ /// This method only preserves the file permissions bits (via a `& 0o777`) and discards
+ /// higher file mode bits. So it cannot be used to denote an entry as a directory,
+ /// symlink, or other special file type.
+ #[must_use]
+ pub fn unix_permissions(mut self, mode: u32) -> FileOptions {
+ self.permissions = Some(mode & 0o777);
+ self
+ }
+
+ /// Set whether the new file's compressed and uncompressed size is less than 4 GiB.
+ ///
+ /// If set to `false` and the file exceeds the limit, an I/O error is thrown. If set to `true`,
+ /// readers will require ZIP64 support and if the file does not exceed the limit, 20 B are
+ /// wasted. The default is `false`.
+ #[must_use]
+ pub fn large_file(mut self, large: bool) -> FileOptions {
+ self.large_file = large;
+ self
+ }
+}
+
+impl Default for FileOptions {
+ fn default() -> Self {
+ Self::default()
+ }
+}
+
+impl<W: Write + io::Seek> Write for ZipWriter<W> {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ if !self.writing_to_file {
+ return Err(io::Error::new(
+ io::ErrorKind::Other,
+ "No file has been started",
+ ));
+ }
+ match self.inner.ref_mut() {
+ Some(ref mut w) => {
+ if self.writing_to_extra_field {
+ self.files.last_mut().unwrap().extra_field.write(buf)
+ } else {
+ let write_result = w.write(buf);
+ if let Ok(count) = write_result {
+ self.stats.update(&buf[0..count]);
+ if self.stats.bytes_written > spec::ZIP64_BYTES_THR
+ && !self.files.last_mut().unwrap().large_file
+ {
+ let _inner = mem::replace(&mut self.inner, GenericZipWriter::Closed);
+ return Err(io::Error::new(
+ io::ErrorKind::Other,
+ "Large file option has not been set",
+ ));
+ }
+ }
+ write_result
+ }
+ }
+ None => Err(io::Error::new(
+ io::ErrorKind::BrokenPipe,
+ "ZipWriter was already closed",
+ )),
+ }
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ match self.inner.ref_mut() {
+ Some(ref mut w) => w.flush(),
+ None => Err(io::Error::new(
+ io::ErrorKind::BrokenPipe,
+ "ZipWriter was already closed",
+ )),
+ }
+ }
+}
+
+impl ZipWriterStats {
+ fn update(&mut self, buf: &[u8]) {
+ self.hasher.update(buf);
+ self.bytes_written += buf.len() as u64;
+ }
+}
+
+impl<A: Read + Write + io::Seek> ZipWriter<A> {
+ /// Initializes the archive from an existing ZIP archive, making it ready for append.
+ pub fn new_append(mut readwriter: A) -> ZipResult<ZipWriter<A>> {
+ let (footer, cde_start_pos) = spec::CentralDirectoryEnd::find_and_parse(&mut readwriter)?;
+
+ if footer.disk_number != footer.disk_with_central_directory {
+ return Err(ZipError::UnsupportedArchive(
+ "Support for multi-disk files is not implemented",
+ ));
+ }
+
+ let (archive_offset, directory_start, number_of_files) =
+ ZipArchive::get_directory_counts(&mut readwriter, &footer, cde_start_pos)?;
+
+ if readwriter
+ .seek(io::SeekFrom::Start(directory_start))
+ .is_err()
+ {
+ return Err(ZipError::InvalidArchive(
+ "Could not seek to start of central directory",
+ ));
+ }
+
+ let files = (0..number_of_files)
+ .map(|_| central_header_to_zip_file(&mut readwriter, archive_offset))
+ .collect::<Result<Vec<_>, _>>()?;
+
+ let _ = readwriter.seek(io::SeekFrom::Start(directory_start)); // seek directory_start to overwrite it
+
+ Ok(ZipWriter {
+ inner: GenericZipWriter::Storer(readwriter),
+ files,
+ stats: Default::default(),
+ writing_to_file: false,
+ writing_to_extra_field: false,
+ writing_to_central_extra_field_only: false,
+ comment: footer.zip_file_comment,
+ writing_raw: true, // avoid recomputing the last file's header
+ })
+ }
+}
+
+impl<W: Write + io::Seek> ZipWriter<W> {
+ /// Initializes the archive.
+ ///
+ /// Before writing to this object, the [`ZipWriter::start_file`] function should be called.
+ pub fn new(inner: W) -> ZipWriter<W> {
+ ZipWriter {
+ inner: GenericZipWriter::Storer(inner),
+ files: Vec::new(),
+ stats: Default::default(),
+ writing_to_file: false,
+ writing_to_extra_field: false,
+ writing_to_central_extra_field_only: false,
+ writing_raw: false,
+ comment: Vec::new(),
+ }
+ }
+
+ /// Set ZIP archive comment.
+ pub fn set_comment<S>(&mut self, comment: S)
+ where
+ S: Into<String>,
+ {
+ self.set_raw_comment(comment.into().into())
+ }
+
+ /// Set ZIP archive comment.
+ ///
+ /// This sets the raw bytes of the comment. The comment
+ /// is typically expected to be encoded in UTF-8
+ pub fn set_raw_comment(&mut self, comment: Vec<u8>) {
+ self.comment = comment;
+ }
+
+ /// Start a new file for with the requested options.
+ fn start_entry<S>(
+ &mut self,
+ name: S,
+ options: FileOptions,
+ raw_values: Option<ZipRawValues>,
+ ) -> ZipResult<()>
+ where
+ S: Into<String>,
+ {
+ self.finish_file()?;
+
+ let raw_values = raw_values.unwrap_or(ZipRawValues {
+ crc32: 0,
+ compressed_size: 0,
+ uncompressed_size: 0,
+ });
+
+ {
+ let writer = self.inner.get_plain();
+ let header_start = writer.stream_position()?;
+
+ let permissions = options.permissions.unwrap_or(0o100644);
+ let mut file = ZipFileData {
+ system: System::Unix,
+ version_made_by: DEFAULT_VERSION,
+ encrypted: false,
+ using_data_descriptor: false,
+ compression_method: options.compression_method,
+ compression_level: options.compression_level,
+ last_modified_time: options.last_modified_time,
+ crc32: raw_values.crc32,
+ compressed_size: raw_values.compressed_size,
+ uncompressed_size: raw_values.uncompressed_size,
+ file_name: name.into(),
+ file_name_raw: Vec::new(), // Never used for saving
+ extra_field: Vec::new(),
+ file_comment: String::new(),
+ header_start,
+ data_start: AtomicU64::new(0),
+ central_header_start: 0,
+ external_attributes: permissions << 16,
+ large_file: options.large_file,
+ aes_mode: None,
+ };
+ write_local_file_header(writer, &file)?;
+
+ let header_end = writer.stream_position()?;
+ self.stats.start = header_end;
+ *file.data_start.get_mut() = header_end;
+
+ self.stats.bytes_written = 0;
+ self.stats.hasher = Hasher::new();
+
+ self.files.push(file);
+ }
+
+ Ok(())
+ }
+
+ fn finish_file(&mut self) -> ZipResult<()> {
+ if self.writing_to_extra_field {
+ // Implicitly calling [`ZipWriter::end_extra_data`] for empty files.
+ self.end_extra_data()?;
+ }
+ self.inner.switch_to(CompressionMethod::Stored, None)?;
+ let writer = self.inner.get_plain();
+
+ if !self.writing_raw {
+ let file = match self.files.last_mut() {
+ None => return Ok(()),
+ Some(f) => f,
+ };
+ file.crc32 = self.stats.hasher.clone().finalize();
+ file.uncompressed_size = self.stats.bytes_written;
+
+ let file_end = writer.stream_position()?;
+ file.compressed_size = file_end - self.stats.start;
+
+ update_local_file_header(writer, file)?;
+ writer.seek(io::SeekFrom::Start(file_end))?;
+ }
+
+ self.writing_to_file = false;
+ self.writing_raw = false;
+ Ok(())
+ }
+
+ /// Create a file in the archive and start writing its' contents.
+ ///
+ /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`]
+ pub fn start_file<S>(&mut self, name: S, mut options: FileOptions) -> ZipResult<()>
+ where
+ S: Into<String>,
+ {
+ if options.permissions.is_none() {
+ options.permissions = Some(0o644);
+ }
+ *options.permissions.as_mut().unwrap() |= 0o100000;
+ self.start_entry(name, options, None)?;
+ self.inner
+ .switch_to(options.compression_method, options.compression_level)?;
+ self.writing_to_file = true;
+ Ok(())
+ }
+
+ /// Starts a file, taking a Path as argument.
+ ///
+ /// This function ensures that the '/' path separator is used. It also ignores all non 'Normal'
+ /// Components, such as a starting '/' or '..' and '.'.
+ #[deprecated(
+ since = "0.5.7",
+ note = "by stripping `..`s from the path, the meaning of paths can change. Use `start_file` instead."
+ )]
+ pub fn start_file_from_path(
+ &mut self,
+ path: &std::path::Path,
+ options: FileOptions,
+ ) -> ZipResult<()> {
+ self.start_file(path_to_string(path), options)
+ }
+
+ /// Create an aligned file in the archive and start writing its' contents.
+ ///
+ /// Returns the number of padding bytes required to align the file.
+ ///
+ /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`]
+ pub fn start_file_aligned<S>(
+ &mut self,
+ name: S,
+ options: FileOptions,
+ align: u16,
+ ) -> Result<u64, ZipError>
+ where
+ S: Into<String>,
+ {
+ let data_start = self.start_file_with_extra_data(name, options)?;
+ let align = align as u64;
+ if align > 1 && data_start % align != 0 {
+ let pad_length = (align - (data_start + 4) % align) % align;
+ let pad = vec![0; pad_length as usize];
+ self.write_all(b"za").map_err(ZipError::from)?; // 0x617a
+ self.write_u16::<LittleEndian>(pad.len() as u16)
+ .map_err(ZipError::from)?;
+ self.write_all(&pad).map_err(ZipError::from)?;
+ assert_eq!(self.end_local_start_central_extra_data()? % align, 0);
+ }
+ let extra_data_end = self.end_extra_data()?;
+ Ok(extra_data_end - data_start)
+ }
+
+ /// Create a file in the archive and start writing its extra data first.
+ ///
+ /// Finish writing extra data and start writing file data with [`ZipWriter::end_extra_data`].
+ /// Optionally, distinguish local from central extra data with
+ /// [`ZipWriter::end_local_start_central_extra_data`].
+ ///
+ /// Returns the preliminary starting offset of the file data without any extra data allowing to
+ /// align the file data by calculating a pad length to be prepended as part of the extra data.
+ ///
+ /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`]
+ ///
+ /// ```
+ /// use byteorder::{LittleEndian, WriteBytesExt};
+ /// use zip::{ZipArchive, ZipWriter, result::ZipResult};
+ /// use zip::{write::FileOptions, CompressionMethod};
+ /// use std::io::{Write, Cursor};
+ ///
+ /// # fn main() -> ZipResult<()> {
+ /// let mut archive = Cursor::new(Vec::new());
+ ///
+ /// {
+ /// let mut zip = ZipWriter::new(&mut archive);
+ /// let options = FileOptions::default()
+ /// .compression_method(CompressionMethod::Stored);
+ ///
+ /// zip.start_file_with_extra_data("identical_extra_data.txt", options)?;
+ /// let extra_data = b"local and central extra data";
+ /// zip.write_u16::<LittleEndian>(0xbeef)?;
+ /// zip.write_u16::<LittleEndian>(extra_data.len() as u16)?;
+ /// zip.write_all(extra_data)?;
+ /// zip.end_extra_data()?;
+ /// zip.write_all(b"file data")?;
+ ///
+ /// let data_start = zip.start_file_with_extra_data("different_extra_data.txt", options)?;
+ /// let extra_data = b"local extra data";
+ /// zip.write_u16::<LittleEndian>(0xbeef)?;
+ /// zip.write_u16::<LittleEndian>(extra_data.len() as u16)?;
+ /// zip.write_all(extra_data)?;
+ /// let data_start = data_start as usize + 4 + extra_data.len() + 4;
+ /// let align = 64;
+ /// let pad_length = (align - data_start % align) % align;
+ /// assert_eq!(pad_length, 19);
+ /// zip.write_u16::<LittleEndian>(0xdead)?;
+ /// zip.write_u16::<LittleEndian>(pad_length as u16)?;
+ /// zip.write_all(&vec![0; pad_length])?;
+ /// let data_start = zip.end_local_start_central_extra_data()?;
+ /// assert_eq!(data_start as usize % align, 0);
+ /// let extra_data = b"central extra data";
+ /// zip.write_u16::<LittleEndian>(0xbeef)?;
+ /// zip.write_u16::<LittleEndian>(extra_data.len() as u16)?;
+ /// zip.write_all(extra_data)?;
+ /// zip.end_extra_data()?;
+ /// zip.write_all(b"file data")?;
+ ///
+ /// zip.finish()?;
+ /// }
+ ///
+ /// let mut zip = ZipArchive::new(archive)?;
+ /// assert_eq!(&zip.by_index(0)?.extra_data()[4..], b"local and central extra data");
+ /// assert_eq!(&zip.by_index(1)?.extra_data()[4..], b"central extra data");
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn start_file_with_extra_data<S>(
+ &mut self,
+ name: S,
+ mut options: FileOptions,
+ ) -> ZipResult<u64>
+ where
+ S: Into<String>,
+ {
+ if options.permissions.is_none() {
+ options.permissions = Some(0o644);
+ }
+ *options.permissions.as_mut().unwrap() |= 0o100000;
+ self.start_entry(name, options, None)?;
+ self.writing_to_file = true;
+ self.writing_to_extra_field = true;
+ Ok(self.files.last().unwrap().data_start.load())
+ }
+
+ /// End local and start central extra data. Requires [`ZipWriter::start_file_with_extra_data`].
+ ///
+ /// Returns the final starting offset of the file data.
+ pub fn end_local_start_central_extra_data(&mut self) -> ZipResult<u64> {
+ let data_start = self.end_extra_data()?;
+ self.files.last_mut().unwrap().extra_field.clear();
+ self.writing_to_extra_field = true;
+ self.writing_to_central_extra_field_only = true;
+ Ok(data_start)
+ }
+
+ /// End extra data and start file data. Requires [`ZipWriter::start_file_with_extra_data`].
+ ///
+ /// Returns the final starting offset of the file data.
+ pub fn end_extra_data(&mut self) -> ZipResult<u64> {
+ // Require `start_file_with_extra_data()`. Ensures `file` is some.
+ if !self.writing_to_extra_field {
+ return Err(ZipError::Io(io::Error::new(
+ io::ErrorKind::Other,
+ "Not writing to extra field",
+ )));
+ }
+ let file = self.files.last_mut().unwrap();
+
+ validate_extra_data(file)?;
+
+ let data_start = file.data_start.get_mut();
+
+ if !self.writing_to_central_extra_field_only {
+ let writer = self.inner.get_plain();
+
+ // Append extra data to local file header and keep it for central file header.
+ writer.write_all(&file.extra_field)?;
+
+ // Update final `data_start`.
+ let header_end = *data_start + file.extra_field.len() as u64;
+ self.stats.start = header_end;
+ *data_start = header_end;
+
+ // Update extra field length in local file header.
+ let extra_field_length =
+ if file.large_file { 20 } else { 0 } + file.extra_field.len() as u16;
+ writer.seek(io::SeekFrom::Start(file.header_start + 28))?;
+ writer.write_u16::<LittleEndian>(extra_field_length)?;
+ writer.seek(io::SeekFrom::Start(header_end))?;
+
+ self.inner
+ .switch_to(file.compression_method, file.compression_level)?;
+ }
+
+ self.writing_to_extra_field = false;
+ self.writing_to_central_extra_field_only = false;
+ Ok(*data_start)
+ }
+
+ /// Add a new file using the already compressed data from a ZIP file being read and renames it, this
+ /// allows faster copies of the `ZipFile` since there is no need to decompress and compress it again.
+ /// Any `ZipFile` metadata is copied and not checked, for example the file CRC.
+
+ /// ```no_run
+ /// use std::fs::File;
+ /// use std::io::{Read, Seek, Write};
+ /// use zip::{ZipArchive, ZipWriter};
+ ///
+ /// fn copy_rename<R, W>(
+ /// src: &mut ZipArchive<R>,
+ /// dst: &mut ZipWriter<W>,
+ /// ) -> zip::result::ZipResult<()>
+ /// where
+ /// R: Read + Seek,
+ /// W: Write + Seek,
+ /// {
+ /// // Retrieve file entry by name
+ /// let file = src.by_name("src_file.txt")?;
+ ///
+ /// // Copy and rename the previously obtained file entry to the destination zip archive
+ /// dst.raw_copy_file_rename(file, "new_name.txt")?;
+ ///
+ /// Ok(())
+ /// }
+ /// ```
+ pub fn raw_copy_file_rename<S>(&mut self, mut file: ZipFile, name: S) -> ZipResult<()>
+ where
+ S: Into<String>,
+ {
+ let mut options = FileOptions::default()
+ .large_file(file.compressed_size().max(file.size()) > spec::ZIP64_BYTES_THR)
+ .last_modified_time(file.last_modified())
+ .compression_method(file.compression());
+ if let Some(perms) = file.unix_mode() {
+ options = options.unix_permissions(perms);
+ }
+
+ let raw_values = ZipRawValues {
+ crc32: file.crc32(),
+ compressed_size: file.compressed_size(),
+ uncompressed_size: file.size(),
+ };
+
+ self.start_entry(name, options, Some(raw_values))?;
+ self.writing_to_file = true;
+ self.writing_raw = true;
+
+ io::copy(file.get_raw_reader(), self)?;
+
+ Ok(())
+ }
+
+ /// Add a new file using the already compressed data from a ZIP file being read, this allows faster
+ /// copies of the `ZipFile` since there is no need to decompress and compress it again. Any `ZipFile`
+ /// metadata is copied and not checked, for example the file CRC.
+ ///
+ /// ```no_run
+ /// use std::fs::File;
+ /// use std::io::{Read, Seek, Write};
+ /// use zip::{ZipArchive, ZipWriter};
+ ///
+ /// fn copy<R, W>(src: &mut ZipArchive<R>, dst: &mut ZipWriter<W>) -> zip::result::ZipResult<()>
+ /// where
+ /// R: Read + Seek,
+ /// W: Write + Seek,
+ /// {
+ /// // Retrieve file entry by name
+ /// let file = src.by_name("src_file.txt")?;
+ ///
+ /// // Copy the previously obtained file entry to the destination zip archive
+ /// dst.raw_copy_file(file)?;
+ ///
+ /// Ok(())
+ /// }
+ /// ```
+ pub fn raw_copy_file(&mut self, file: ZipFile) -> ZipResult<()> {
+ let name = file.name().to_owned();
+ self.raw_copy_file_rename(file, name)
+ }
+
+ /// Add a directory entry.
+ ///
+ /// You can't write data to the file afterwards.
+ pub fn add_directory<S>(&mut self, name: S, mut options: FileOptions) -> ZipResult<()>
+ where
+ S: Into<String>,
+ {
+ if options.permissions.is_none() {
+ options.permissions = Some(0o755);
+ }
+ *options.permissions.as_mut().unwrap() |= 0o40000;
+ options.compression_method = CompressionMethod::Stored;
+
+ let name_as_string = name.into();
+ // Append a slash to the filename if it does not end with it.
+ let name_with_slash = match name_as_string.chars().last() {
+ Some('/') | Some('\\') => name_as_string,
+ _ => name_as_string + "/",
+ };
+
+ self.start_entry(name_with_slash, options, None)?;
+ self.writing_to_file = false;
+ Ok(())
+ }
+
+ /// Add a directory entry, taking a Path as argument.
+ ///
+ /// This function ensures that the '/' path separator is used. It also ignores all non 'Normal'
+ /// Components, such as a starting '/' or '..' and '.'.
+ #[deprecated(
+ since = "0.5.7",
+ note = "by stripping `..`s from the path, the meaning of paths can change. Use `add_directory` instead."
+ )]
+ pub fn add_directory_from_path(
+ &mut self,
+ path: &std::path::Path,
+ options: FileOptions,
+ ) -> ZipResult<()> {
+ self.add_directory(path_to_string(path), options)
+ }
+
+ /// Finish the last file and write all other zip-structures
+ ///
+ /// This will return the writer, but one should normally not append any data to the end of the file.
+ /// Note that the zipfile will also be finished on drop.
+ pub fn finish(&mut self) -> ZipResult<W> {
+ self.finalize()?;
+ let inner = mem::replace(&mut self.inner, GenericZipWriter::Closed);
+ Ok(inner.unwrap())
+ }
+
+ /// Add a symlink entry.
+ ///
+ /// The zip archive will contain an entry for path `name` which is a symlink to `target`.
+ ///
+ /// No validation or normalization of the paths is performed. For best results,
+ /// callers should normalize `\` to `/` and ensure symlinks are relative to other
+ /// paths within the zip archive.
+ ///
+ /// WARNING: not all zip implementations preserve symlinks on extract. Some zip
+ /// implementations may materialize a symlink as a regular file, possibly with the
+ /// content incorrectly set to the symlink target. For maximum portability, consider
+ /// storing a regular file instead.
+ pub fn add_symlink<N, T>(
+ &mut self,
+ name: N,
+ target: T,
+ mut options: FileOptions,
+ ) -> ZipResult<()>
+ where
+ N: Into<String>,
+ T: Into<String>,
+ {
+ if options.permissions.is_none() {
+ options.permissions = Some(0o777);
+ }
+ *options.permissions.as_mut().unwrap() |= 0o120000;
+ // The symlink target is stored as file content. And compressing the target path
+ // likely wastes space. So always store.
+ options.compression_method = CompressionMethod::Stored;
+
+ self.start_entry(name, options, None)?;
+ self.writing_to_file = true;
+ self.write_all(target.into().as_bytes())?;
+ self.writing_to_file = false;
+
+ Ok(())
+ }
+
+ fn finalize(&mut self) -> ZipResult<()> {
+ self.finish_file()?;
+
+ {
+ let writer = self.inner.get_plain();
+
+ let central_start = writer.stream_position()?;
+ for file in self.files.iter() {
+ write_central_directory_header(writer, file)?;
+ }
+ let central_size = writer.stream_position()? - central_start;
+
+ if self.files.len() > spec::ZIP64_ENTRY_THR
+ || central_size.max(central_start) > spec::ZIP64_BYTES_THR
+ {
+ let zip64_footer = spec::Zip64CentralDirectoryEnd {
+ version_made_by: DEFAULT_VERSION as u16,
+ version_needed_to_extract: DEFAULT_VERSION as u16,
+ disk_number: 0,
+ disk_with_central_directory: 0,
+ number_of_files_on_this_disk: self.files.len() as u64,
+ number_of_files: self.files.len() as u64,
+ central_directory_size: central_size,
+ central_directory_offset: central_start,
+ };
+
+ zip64_footer.write(writer)?;
+
+ let zip64_footer = spec::Zip64CentralDirectoryEndLocator {
+ disk_with_central_directory: 0,
+ end_of_central_directory_offset: central_start + central_size,
+ number_of_disks: 1,
+ };
+
+ zip64_footer.write(writer)?;
+ }
+
+ let number_of_files = self.files.len().min(spec::ZIP64_ENTRY_THR) as u16;
+ let footer = spec::CentralDirectoryEnd {
+ disk_number: 0,
+ disk_with_central_directory: 0,
+ zip_file_comment: self.comment.clone(),
+ number_of_files_on_this_disk: number_of_files,
+ number_of_files,
+ central_directory_size: central_size.min(spec::ZIP64_BYTES_THR) as u32,
+ central_directory_offset: central_start.min(spec::ZIP64_BYTES_THR) as u32,
+ };
+
+ footer.write(writer)?;
+ }
+
+ Ok(())
+ }
+}
+
+impl<W: Write + io::Seek> Drop for ZipWriter<W> {
+ fn drop(&mut self) {
+ if !self.inner.is_closed() {
+ if let Err(e) = self.finalize() {
+ let _ = write!(io::stderr(), "ZipWriter drop failed: {:?}", e);
+ }
+ }
+ }
+}
+
+impl<W: Write + io::Seek> GenericZipWriter<W> {
+ fn switch_to(
+ &mut self,
+ compression: CompressionMethod,
+ compression_level: Option<i32>,
+ ) -> ZipResult<()> {
+ match self.current_compression() {
+ Some(method) if method == compression => return Ok(()),
+ None => {
+ return Err(io::Error::new(
+ io::ErrorKind::BrokenPipe,
+ "ZipWriter was already closed",
+ )
+ .into())
+ }
+ _ => {}
+ }
+
+ let bare = match mem::replace(self, GenericZipWriter::Closed) {
+ GenericZipWriter::Storer(w) => w,
+ #[cfg(any(
+ feature = "deflate",
+ feature = "deflate-miniz",
+ feature = "deflate-zlib"
+ ))]
+ GenericZipWriter::Deflater(w) => w.finish()?,
+ #[cfg(feature = "bzip2")]
+ GenericZipWriter::Bzip2(w) => w.finish()?,
+ #[cfg(feature = "zstd")]
+ GenericZipWriter::Zstd(w) => w.finish()?,
+ GenericZipWriter::Closed => {
+ return Err(io::Error::new(
+ io::ErrorKind::BrokenPipe,
+ "ZipWriter was already closed",
+ )
+ .into())
+ }
+ };
+
+ *self = {
+ #[allow(deprecated)]
+ match compression {
+ CompressionMethod::Stored => {
+ if compression_level.is_some() {
+ return Err(ZipError::UnsupportedArchive(
+ "Unsupported compression level",
+ ));
+ }
+
+ GenericZipWriter::Storer(bare)
+ }
+ #[cfg(any(
+ feature = "deflate",
+ feature = "deflate-miniz",
+ feature = "deflate-zlib"
+ ))]
+ CompressionMethod::Deflated => GenericZipWriter::Deflater(DeflateEncoder::new(
+ bare,
+ flate2::Compression::new(
+ clamp_opt(
+ compression_level
+ .unwrap_or(flate2::Compression::default().level() as i32),
+ deflate_compression_level_range(),
+ )
+ .ok_or(ZipError::UnsupportedArchive(
+ "Unsupported compression level",
+ ))? as u32,
+ ),
+ )),
+ #[cfg(feature = "bzip2")]
+ CompressionMethod::Bzip2 => GenericZipWriter::Bzip2(BzEncoder::new(
+ bare,
+ bzip2::Compression::new(
+ clamp_opt(
+ compression_level
+ .unwrap_or(bzip2::Compression::default().level() as i32),
+ bzip2_compression_level_range(),
+ )
+ .ok_or(ZipError::UnsupportedArchive(
+ "Unsupported compression level",
+ ))? as u32,
+ ),
+ )),
+ CompressionMethod::AES => {
+ return Err(ZipError::UnsupportedArchive(
+ "AES compression is not supported for writing",
+ ))
+ }
+ #[cfg(feature = "zstd")]
+ CompressionMethod::Zstd => GenericZipWriter::Zstd(
+ ZstdEncoder::new(
+ bare,
+ clamp_opt(
+ compression_level.unwrap_or(zstd::DEFAULT_COMPRESSION_LEVEL),
+ zstd::compression_level_range(),
+ )
+ .ok_or(ZipError::UnsupportedArchive(
+ "Unsupported compression level",
+ ))?,
+ )
+ .unwrap(),
+ ),
+ CompressionMethod::Unsupported(..) => {
+ return Err(ZipError::UnsupportedArchive("Unsupported compression"))
+ }
+ }
+ };
+
+ Ok(())
+ }
+
+ fn ref_mut(&mut self) -> Option<&mut dyn Write> {
+ match *self {
+ GenericZipWriter::Storer(ref mut w) => Some(w as &mut dyn Write),
+ #[cfg(any(
+ feature = "deflate",
+ feature = "deflate-miniz",
+ feature = "deflate-zlib"
+ ))]
+ GenericZipWriter::Deflater(ref mut w) => Some(w as &mut dyn Write),
+ #[cfg(feature = "bzip2")]
+ GenericZipWriter::Bzip2(ref mut w) => Some(w as &mut dyn Write),
+ #[cfg(feature = "zstd")]
+ GenericZipWriter::Zstd(ref mut w) => Some(w as &mut dyn Write),
+ GenericZipWriter::Closed => None,
+ }
+ }
+
+ fn is_closed(&self) -> bool {
+ matches!(*self, GenericZipWriter::Closed)
+ }
+
+ fn get_plain(&mut self) -> &mut W {
+ match *self {
+ GenericZipWriter::Storer(ref mut w) => w,
+ _ => panic!("Should have switched to stored beforehand"),
+ }
+ }
+
+ fn current_compression(&self) -> Option<CompressionMethod> {
+ match *self {
+ GenericZipWriter::Storer(..) => Some(CompressionMethod::Stored),
+ #[cfg(any(
+ feature = "deflate",
+ feature = "deflate-miniz",
+ feature = "deflate-zlib"
+ ))]
+ GenericZipWriter::Deflater(..) => Some(CompressionMethod::Deflated),
+ #[cfg(feature = "bzip2")]
+ GenericZipWriter::Bzip2(..) => Some(CompressionMethod::Bzip2),
+ #[cfg(feature = "zstd")]
+ GenericZipWriter::Zstd(..) => Some(CompressionMethod::Zstd),
+ GenericZipWriter::Closed => None,
+ }
+ }
+
+ fn unwrap(self) -> W {
+ match self {
+ GenericZipWriter::Storer(w) => w,
+ _ => panic!("Should have switched to stored beforehand"),
+ }
+ }
+}
+
+#[cfg(any(
+ feature = "deflate",
+ feature = "deflate-miniz",
+ feature = "deflate-zlib"
+))]
+fn deflate_compression_level_range() -> std::ops::RangeInclusive<i32> {
+ let min = flate2::Compression::none().level() as i32;
+ let max = flate2::Compression::best().level() as i32;
+ min..=max
+}
+
+#[cfg(feature = "bzip2")]
+fn bzip2_compression_level_range() -> std::ops::RangeInclusive<i32> {
+ let min = bzip2::Compression::none().level() as i32;
+ let max = bzip2::Compression::best().level() as i32;
+ min..=max
+}
+
+#[cfg(any(
+ feature = "deflate",
+ feature = "deflate-miniz",
+ feature = "deflate-zlib",
+ feature = "bzip2",
+ feature = "zstd"
+))]
+fn clamp_opt<T: Ord + Copy>(value: T, range: std::ops::RangeInclusive<T>) -> Option<T> {
+ if range.contains(&value) {
+ Some(value)
+ } else {
+ None
+ }
+}
+
+fn write_local_file_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
+ // local file header signature
+ writer.write_u32::<LittleEndian>(spec::LOCAL_FILE_HEADER_SIGNATURE)?;
+ // version needed to extract
+ writer.write_u16::<LittleEndian>(file.version_needed())?;
+ // general purpose bit flag
+ let flag = if !file.file_name.is_ascii() {
+ 1u16 << 11
+ } else {
+ 0
+ };
+ writer.write_u16::<LittleEndian>(flag)?;
+ // Compression method
+ #[allow(deprecated)]
+ writer.write_u16::<LittleEndian>(file.compression_method.to_u16())?;
+ // last mod file time and last mod file date
+ writer.write_u16::<LittleEndian>(file.last_modified_time.timepart())?;
+ writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?;
+ // crc-32
+ writer.write_u32::<LittleEndian>(file.crc32)?;
+ // compressed size and uncompressed size
+ if file.large_file {
+ writer.write_u32::<LittleEndian>(spec::ZIP64_BYTES_THR as u32)?;
+ writer.write_u32::<LittleEndian>(spec::ZIP64_BYTES_THR as u32)?;
+ } else {
+ writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
+ writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
+ }
+ // file name length
+ writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
+ // extra field length
+ let extra_field_length = if file.large_file { 20 } else { 0 } + file.extra_field.len() as u16;
+ writer.write_u16::<LittleEndian>(extra_field_length)?;
+ // file name
+ writer.write_all(file.file_name.as_bytes())?;
+ // zip64 extra field
+ if file.large_file {
+ write_local_zip64_extra_field(writer, file)?;
+ }
+
+ Ok(())
+}
+
+fn update_local_file_header<T: Write + io::Seek>(
+ writer: &mut T,
+ file: &ZipFileData,
+) -> ZipResult<()> {
+ const CRC32_OFFSET: u64 = 14;
+ writer.seek(io::SeekFrom::Start(file.header_start + CRC32_OFFSET))?;
+ writer.write_u32::<LittleEndian>(file.crc32)?;
+ if file.large_file {
+ update_local_zip64_extra_field(writer, file)?;
+ } else {
+ // check compressed size as well as it can also be slightly larger than uncompressed size
+ if file.compressed_size > spec::ZIP64_BYTES_THR {
+ return Err(ZipError::Io(io::Error::new(
+ io::ErrorKind::Other,
+ "Large file option has not been set",
+ )));
+ }
+ writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
+ // uncompressed size is already checked on write to catch it as soon as possible
+ writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
+ }
+ Ok(())
+}
+
+fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
+ // buffer zip64 extra field to determine its variable length
+ let mut zip64_extra_field = [0; 28];
+ let zip64_extra_field_length =
+ write_central_zip64_extra_field(&mut zip64_extra_field.as_mut(), file)?;
+
+ // central file header signature
+ writer.write_u32::<LittleEndian>(spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE)?;
+ // version made by
+ let version_made_by = (file.system as u16) << 8 | (file.version_made_by as u16);
+ writer.write_u16::<LittleEndian>(version_made_by)?;
+ // version needed to extract
+ writer.write_u16::<LittleEndian>(file.version_needed())?;
+ // general puprose bit flag
+ let flag = if !file.file_name.is_ascii() {
+ 1u16 << 11
+ } else {
+ 0
+ };
+ writer.write_u16::<LittleEndian>(flag)?;
+ // compression method
+ #[allow(deprecated)]
+ writer.write_u16::<LittleEndian>(file.compression_method.to_u16())?;
+ // last mod file time + date
+ writer.write_u16::<LittleEndian>(file.last_modified_time.timepart())?;
+ writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?;
+ // crc-32
+ writer.write_u32::<LittleEndian>(file.crc32)?;
+ // compressed size
+ writer.write_u32::<LittleEndian>(file.compressed_size.min(spec::ZIP64_BYTES_THR) as u32)?;
+ // uncompressed size
+ writer.write_u32::<LittleEndian>(file.uncompressed_size.min(spec::ZIP64_BYTES_THR) as u32)?;
+ // file name length
+ writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
+ // extra field length
+ writer.write_u16::<LittleEndian>(zip64_extra_field_length + file.extra_field.len() as u16)?;
+ // file comment length
+ writer.write_u16::<LittleEndian>(0)?;
+ // disk number start
+ writer.write_u16::<LittleEndian>(0)?;
+ // internal file attribytes
+ writer.write_u16::<LittleEndian>(0)?;
+ // external file attributes
+ writer.write_u32::<LittleEndian>(file.external_attributes)?;
+ // relative offset of local header
+ writer.write_u32::<LittleEndian>(file.header_start.min(spec::ZIP64_BYTES_THR) as u32)?;
+ // file name
+ writer.write_all(file.file_name.as_bytes())?;
+ // zip64 extra field
+ writer.write_all(&zip64_extra_field[..zip64_extra_field_length as usize])?;
+ // extra field
+ writer.write_all(&file.extra_field)?;
+ // file comment
+ // <none>
+
+ Ok(())
+}
+
+fn validate_extra_data(file: &ZipFileData) -> ZipResult<()> {
+ let mut data = file.extra_field.as_slice();
+
+ if data.len() > spec::ZIP64_ENTRY_THR {
+ return Err(ZipError::Io(io::Error::new(
+ io::ErrorKind::InvalidData,
+ "Extra data exceeds extra field",
+ )));
+ }
+
+ while !data.is_empty() {
+ let left = data.len();
+ if left < 4 {
+ return Err(ZipError::Io(io::Error::new(
+ io::ErrorKind::Other,
+ "Incomplete extra data header",
+ )));
+ }
+ let kind = data.read_u16::<LittleEndian>()?;
+ let size = data.read_u16::<LittleEndian>()? as usize;
+ let left = left - 4;
+
+ if kind == 0x0001 {
+ return Err(ZipError::Io(io::Error::new(
+ io::ErrorKind::Other,
+ "No custom ZIP64 extra data allowed",
+ )));
+ }
+
+ #[cfg(not(feature = "unreserved"))]
+ {
+ if kind <= 31 || EXTRA_FIELD_MAPPING.iter().any(|&mapped| mapped == kind) {
+ return Err(ZipError::Io(io::Error::new(
+ io::ErrorKind::Other,
+ format!(
+ "Extra data header ID {:#06} requires crate feature \"unreserved\"",
+ kind,
+ ),
+ )));
+ }
+ }
+
+ if size > left {
+ return Err(ZipError::Io(io::Error::new(
+ io::ErrorKind::Other,
+ "Extra data size exceeds extra field",
+ )));
+ }
+
+ data = &data[size..];
+ }
+
+ Ok(())
+}
+
+fn write_local_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
+ // This entry in the Local header MUST include BOTH original
+ // and compressed file size fields.
+ writer.write_u16::<LittleEndian>(0x0001)?;
+ writer.write_u16::<LittleEndian>(16)?;
+ writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
+ writer.write_u64::<LittleEndian>(file.compressed_size)?;
+ // Excluded fields:
+ // u32: disk start number
+ Ok(())
+}
+
+fn update_local_zip64_extra_field<T: Write + io::Seek>(
+ writer: &mut T,
+ file: &ZipFileData,
+) -> ZipResult<()> {
+ let zip64_extra_field = file.header_start + 30 + file.file_name.as_bytes().len() as u64;
+ writer.seek(io::SeekFrom::Start(zip64_extra_field + 4))?;
+ writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
+ writer.write_u64::<LittleEndian>(file.compressed_size)?;
+ // Excluded fields:
+ // u32: disk start number
+ Ok(())
+}
+
+fn write_central_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<u16> {
+ // The order of the fields in the zip64 extended
+ // information record is fixed, but the fields MUST
+ // only appear if the corresponding Local or Central
+ // directory record field is set to 0xFFFF or 0xFFFFFFFF.
+ let mut size = 0;
+ let uncompressed_size = file.uncompressed_size > spec::ZIP64_BYTES_THR;
+ let compressed_size = file.compressed_size > spec::ZIP64_BYTES_THR;
+ let header_start = file.header_start > spec::ZIP64_BYTES_THR;
+ if uncompressed_size {
+ size += 8;
+ }
+ if compressed_size {
+ size += 8;
+ }
+ if header_start {
+ size += 8;
+ }
+ if size > 0 {
+ writer.write_u16::<LittleEndian>(0x0001)?;
+ writer.write_u16::<LittleEndian>(size)?;
+ size += 4;
+
+ if uncompressed_size {
+ writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
+ }
+ if compressed_size {
+ writer.write_u64::<LittleEndian>(file.compressed_size)?;
+ }
+ if header_start {
+ writer.write_u64::<LittleEndian>(file.header_start)?;
+ }
+ // Excluded fields:
+ // u32: disk start number
+ }
+ Ok(size)
+}
+
+fn path_to_string(path: &std::path::Path) -> String {
+ let mut path_str = String::new();
+ for component in path.components() {
+ if let std::path::Component::Normal(os_str) = component {
+ if !path_str.is_empty() {
+ path_str.push('/');
+ }
+ path_str.push_str(&*os_str.to_string_lossy());
+ }
+ }
+ path_str
+}
+
+#[cfg(test)]
+mod test {
+ use super::{FileOptions, ZipWriter};
+ use crate::compression::CompressionMethod;
+ use crate::types::DateTime;
+ use std::io;
+ use std::io::Write;
+
+ #[test]
+ fn write_empty_zip() {
+ let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
+ writer.set_comment("ZIP");
+ let result = writer.finish().unwrap();
+ assert_eq!(result.get_ref().len(), 25);
+ assert_eq!(
+ *result.get_ref(),
+ [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]
+ );
+ }
+
+ #[test]
+ fn unix_permissions_bitmask() {
+ // unix_permissions() throws away upper bits.
+ let options = FileOptions::default().unix_permissions(0o120777);
+ assert_eq!(options.permissions, Some(0o777));
+ }
+
+ #[test]
+ fn write_zip_dir() {
+ let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
+ writer
+ .add_directory(
+ "test",
+ FileOptions::default().last_modified_time(
+ DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
+ ),
+ )
+ .unwrap();
+ assert!(writer
+ .write(b"writing to a directory is not allowed, and will not write any data")
+ .is_err());
+ let result = writer.finish().unwrap();
+ assert_eq!(result.get_ref().len(), 108);
+ assert_eq!(
+ *result.get_ref(),
+ &[
+ 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 5, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 1, 2, 46, 3, 20, 0, 0, 0, 0, 0,
+ 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,
+ 0, 0, 237, 65, 0, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0,
+ 1, 0, 51, 0, 0, 0, 35, 0, 0, 0, 0, 0,
+ ] as &[u8]
+ );
+ }
+
+ #[test]
+ fn write_symlink_simple() {
+ let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
+ writer
+ .add_symlink(
+ "name",
+ "target",
+ FileOptions::default().last_modified_time(
+ DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
+ ),
+ )
+ .unwrap();
+ assert!(writer
+ .write(b"writing to a symlink is not allowed and will not write any data")
+ .is_err());
+ let result = writer.finish().unwrap();
+ assert_eq!(result.get_ref().len(), 112);
+ assert_eq!(
+ *result.get_ref(),
+ &[
+ 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 252, 47, 111, 70, 6, 0, 0, 0,
+ 6, 0, 0, 0, 4, 0, 0, 0, 110, 97, 109, 101, 116, 97, 114, 103, 101, 116, 80, 75, 1,
+ 2, 46, 3, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 252, 47, 111, 70, 6, 0, 0, 0, 6, 0,
+ 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 161, 0, 0, 0, 0, 110, 97, 109, 101,
+ 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 50, 0, 0, 0, 40, 0, 0, 0, 0, 0
+ ] as &[u8],
+ );
+ }
+
+ #[test]
+ fn write_symlink_wonky_paths() {
+ let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
+ writer
+ .add_symlink(
+ "directory\\link",
+ "/absolute/symlink\\with\\mixed/slashes",
+ FileOptions::default().last_modified_time(
+ DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
+ ),
+ )
+ .unwrap();
+ assert!(writer
+ .write(b"writing to a symlink is not allowed and will not write any data")
+ .is_err());
+ let result = writer.finish().unwrap();
+ assert_eq!(result.get_ref().len(), 162);
+ assert_eq!(
+ *result.get_ref(),
+ &[
+ 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 95, 41, 81, 245, 36, 0, 0, 0,
+ 36, 0, 0, 0, 14, 0, 0, 0, 100, 105, 114, 101, 99, 116, 111, 114, 121, 92, 108, 105,
+ 110, 107, 47, 97, 98, 115, 111, 108, 117, 116, 101, 47, 115, 121, 109, 108, 105,
+ 110, 107, 92, 119, 105, 116, 104, 92, 109, 105, 120, 101, 100, 47, 115, 108, 97,
+ 115, 104, 101, 115, 80, 75, 1, 2, 46, 3, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 95,
+ 41, 81, 245, 36, 0, 0, 0, 36, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255,
+ 161, 0, 0, 0, 0, 100, 105, 114, 101, 99, 116, 111, 114, 121, 92, 108, 105, 110,
+ 107, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 60, 0, 0, 0, 80, 0, 0, 0, 0, 0
+ ] as &[u8],
+ );
+ }
+
+ #[test]
+ fn write_mimetype_zip() {
+ let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
+ let options = FileOptions {
+ compression_method: CompressionMethod::Stored,
+ compression_level: None,
+ last_modified_time: DateTime::default(),
+ permissions: Some(33188),
+ large_file: false,
+ };
+ writer.start_file("mimetype", options).unwrap();
+ writer
+ .write_all(b"application/vnd.oasis.opendocument.text")
+ .unwrap();
+ let result = writer.finish().unwrap();
+
+ assert_eq!(result.get_ref().len(), 153);
+ let mut v = Vec::new();
+ v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip"));
+ assert_eq!(result.get_ref(), &v);
+ }
+
+ #[test]
+ fn path_to_string() {
+ let mut path = std::path::PathBuf::new();
+ #[cfg(windows)]
+ path.push(r"C:\");
+ #[cfg(unix)]
+ path.push("/");
+ path.push("windows");
+ path.push("..");
+ path.push(".");
+ path.push("system32");
+ let path_str = super::path_to_string(&path);
+ assert_eq!(path_str, "windows/system32");
+ }
+}
+
+#[cfg(not(feature = "unreserved"))]
+const EXTRA_FIELD_MAPPING: [u16; 49] = [
+ 0x0001, 0x0007, 0x0008, 0x0009, 0x000a, 0x000c, 0x000d, 0x000e, 0x000f, 0x0014, 0x0015, 0x0016,
+ 0x0017, 0x0018, 0x0019, 0x0020, 0x0021, 0x0022, 0x0023, 0x0065, 0x0066, 0x4690, 0x07c8, 0x2605,
+ 0x2705, 0x2805, 0x334d, 0x4341, 0x4453, 0x4704, 0x470f, 0x4b46, 0x4c41, 0x4d49, 0x4f4c, 0x5356,
+ 0x5455, 0x554e, 0x5855, 0x6375, 0x6542, 0x7075, 0x756e, 0x7855, 0xa11e, 0xa220, 0xfd4a, 0x9901,
+ 0x9902,
+];