Source code

Revision control

Copy as Markdown

Other Tools

//! 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::convert::TryInto;
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 {
/// 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 {
/// Construct a new FileOptions object
fn default() -> Self {
Self {
#[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: OffsetDateTime::now_utc().try_into().unwrap_or_default(),
#[cfg(not(feature = "time"))]
last_modified_time: DateTime::default(),
permissions: None,
large_file: false,
}
}
}
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 {kind:#06} requires crate feature \"unreserved\"",
),
)));
}
}
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,
];