// Put the struct declaration in a private module to convince rustdoc to display ZipWriter nicely pub(crate) mod zip_writer { usesuper::*; /// 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::SimpleFileOptions; /// /// // We use a buffer here, though you'd normally use a `File` /// let mut buf = [0; 65536]; /// let mut zip = ZipWriter::new(std::io::Cursor::new(&mut buf[..])); /// /// let options = SimpleFileOptions::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(); /// ``` #[derive(Debug)] pubstruct ZipWriter<W: Write + Seek> { pub(super) inner: GenericZipWriter<W>, pub(super) files: IndexMap<Box<str>, ZipFileData>, pub(super) stats: ZipWriterStats, pub(super) writing_to_file: bool, pub(super) writing_raw: bool, pub(super) comment: Box<[u8]>, pub(super) flush_on_finish_file: bool,
}
} #[doc(inline)] pubuseself::sealed::FileOptionExtension; usecrate::result::ZipError::InvalidArchive; #[cfg(feature = "lzma")] usecrate::result::ZipError::UnsupportedArchive; usecrate::spec::path_to_string; usecrate::unstable::LittleEndianWriteExt; usecrate::write::GenericZipWriter::{Closed, Storer}; usecrate::zipcrypto::ZipCryptoKeys; usecrate::CompressionMethod::Stored; pubuse zip_writer::ZipWriter;
/// Metadata for a file to be written #[derive(Clone, Debug, Copy)] pubstruct FileOptions<'k, T: FileOptionExtension> { pub(crate) compression_method: CompressionMethod, pub(crate) compression_level: Option<i64>, pub(crate) last_modified_time: DateTime, pub(crate) permissions: Option<u32>, pub(crate) large_file: bool, pub(crate) encrypt_with: Option<EncryptWith<'k>>, pub(crate) extended_options: T, pub(crate) alignment: u16, #[cfg(feature = "deflate-zopfli")] pub(super) zopfli_buffer_size: Option<usize>,
} /// Simple File Options. Can be copied and good for simple writing zip files pubtype SimpleFileOptions = FileOptions<'static, ()>; /// Adds Extra Data and Central Extra Data. It does not implement copy. pubtype FullFileOptions<'k> = FileOptions<'k, ExtendedFileOptions>; /// The Extension for Extra Data and Central Extra Data #[derive(Clone, Debug, Default)] pubstruct ExtendedFileOptions {
extra_data: Arc<Vec<u8>>,
central_extra_data: Arc<Vec<u8>>,
}
impl<'k, T: FileOptionExtension> FileOptions<'k, T> { /// Set the compression method for the new file /// /// The default is `CompressionMethod::Deflated` if it is enabled. If not, /// `CompressionMethod::Bzip2` is the default if it is enabled. If neither `bzip2` nor `deflate` /// is enabled, `CompressionMethod::Zlib` is the default. If all else fails, /// `CompressionMethod::Stored` becomes the default and files are written uncompressed. #[must_use] pubconstfn compression_method(mutself, method: CompressionMethod) -> Self { 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`: 10 - 264 for Zopfli, 0 - 9 for other encoders. Default is 24 if Zopfli is the /// only encoder, or 6 otherwise. /// * `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] pubconstfn compression_level(mutself, level: Option<i64>) -> Self { 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] pubconstfn last_modified_time(mutself, mod_time: DateTime) -> Self { 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] pubconstfn unix_permissions(mutself, mode: u32) -> Self { 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 and the file is /// aborted. 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] pubconstfn large_file(mutself, large: bool) -> Self { self.large_file = large; self
}
/// Sets the size of the buffer used to hold the next block that Zopfli will compress. The /// larger the buffer, the more effective the compression, but the more memory is required. /// A value of `None` indicates no buffer, which is recommended only when all non-empty writes /// are larger than about 32 KiB. #[must_use] #[cfg(feature = "deflate-zopfli")] pubconstfn with_zopfli_buffer(mutself, size: Option<usize>) -> Self { self.zopfli_buffer_size = size; self
}
/// Returns the compression level currently set. pubconstfn get_compression_level(&self) -> Option<i64> { self.compression_level
} /// Sets the alignment to the given number of bytes. #[must_use] pubconstfn with_alignment(mutself, alignment: u16) -> Self { self.alignment = alignment; self
}
} impl<'k> FileOptions<'k, ExtendedFileOptions> { /// Adds an extra data field. pubfn add_extra_data(
&mutself,
header_id: u16,
data: &[u8],
central_only: bool,
) -> ZipResult<()> {
validate_extra_data(header_id, data)?; let len = data.len() + 4; ifself.extended_options.extra_data.len()
+ self.extended_options.central_extra_data.len()
+ len
> u16::MAX as usize
{
Err(InvalidArchive( "Extra data field would be longer than allowed",
))
} else { let field = if central_only {
&mutself.extended_options.central_extra_data
} else {
&mutself.extended_options.extra_data
}; let vec = Arc::get_mut(field); let vec = match vec {
Some(exclusive) => exclusive,
None => {
*field = Arc::new(field.to_vec());
Arc::get_mut(field).unwrap()
}
};
vec.reserve_exact(data.len() + 4);
vec.write_u16_le(header_id)?;
vec.write_u16_le(data.len() as u16)?;
vec.write_all(data)?;
Ok(())
}
}
impl<A: Read + Write + Seek> ZipWriter<A> { /// Initializes the archive from an existing ZIP archive, making it ready for append. /// /// This uses a default configuration to initially read the archive. pubfn new_append(readwriter: A) -> ZipResult<ZipWriter<A>> { Self::new_append_with_config(Default::default(), readwriter)
}
/// Initializes the archive from an existing ZIP archive, making it ready for append. /// /// This uses the given read configuration to initially read the archive. pubfn new_append_with_config(config: Config, mut readwriter: A) -> ZipResult<ZipWriter<A>> { let (footer, cde_start_pos) =
spec::Zip32CentralDirectoryEnd::find_and_parse(&mut readwriter)?; let metadata = ZipArchive::get_metadata(config, &mut readwriter, &footer, cde_start_pos)?;
/// `flush_on_finish_file` is designed to support a streaming `inner` that may unload flushed /// bytes. It flushes a file's header and body once it starts writing another file. A ZipWriter /// will not try to seek back into where a previous file was written unless /// either [`ZipWriter::abort_file`] is called while [`ZipWriter::is_writing_file`] returns /// false, or [`ZipWriter::deep_copy_file`] is called. In the latter case, it will only need to /// read previously-written files and not overwrite them. /// /// Note: when using an `inner` that cannot overwrite flushed bytes, do not wrap it in a /// [std::io::BufWriter], because that has a [Seek::seek] method that implicitly calls /// [BufWriter::flush], and ZipWriter needs to seek backward to update each file's header with /// the size and checksum after writing the body. /// /// This setting is false by default. pubfn set_flush_on_finish_file(&mutself, flush_on_finish_file: bool) { self.flush_on_finish_file = flush_on_finish_file;
}
}
impl<A: Read + Write + Seek> ZipWriter<A> { /// Adds another copy of a file already in this archive. This will produce a larger but more /// widely-compatible archive compared to [Self::shallow_copy_file]. Does not copy alignment. pubfn deep_copy_file(&mutself, src_name: &str, dest_name: &str) -> ZipResult<()> { self.finish_file()?; let write_position = self.inner.get_plain().stream_position()?; let src_index = self.index_by_name(src_name)?; let src_data = &self.files[src_index]; let data_start = *src_data.data_start.get().unwrap_or(&0); let compressed_size = src_data.compressed_size;
debug_assert!(compressed_size <= write_position - data_start); let uncompressed_size = src_data.uncompressed_size;
/// Like `deep_copy_file`, but uses Path arguments. /// /// This function ensures that the '/' path separator is used and normalizes `.` and `..`. It /// ignores any `..` or Windows drive letter that would produce a path outside the ZIP file's /// root. pubfn deep_copy_file_from_path<T: AsRef<Path>, U: AsRef<Path>>(
&mutself,
src_path: T,
dest_path: U,
) -> ZipResult<()> { self.deep_copy_file(&path_to_string(src_path), &path_to_string(dest_path))
}
/// Write the zip file into the backing stream, then produce a readable archive of that data. /// /// This method avoids parsing the central directory records at the end of the stream for /// a slight performance improvement over running [`ZipArchive::new()`] on the output of /// [`Self::finish()`]. /// ///``` /// # fn main() -> Result<(), zip::result::ZipError> { /// use std::io::{Cursor, prelude::*}; /// use zip::{ZipArchive, ZipWriter, write::SimpleFileOptions}; /// /// let buf = Cursor::new(Vec::new()); /// let mut zip = ZipWriter::new(buf); /// let options = SimpleFileOptions::default(); /// zip.start_file("a.txt", options)?; /// zip.write_all(b"hello\n")?; /// /// let mut zip = zip.finish_into_readable()?; /// let mut s: String = String::new(); /// zip.by_name("a.txt")?.read_to_string(&mut s)?; /// assert_eq!(s, "hello\n"); /// # Ok(()) /// # } ///``` pubfn finish_into_readable(mutself) -> ZipResult<ZipArchive<A>> { let central_start = self.finalize()?; let inner = mem::replace(&mutself.inner, Closed).unwrap(); let comment = mem::take(&mutself.comment); let files = mem::take(&mutself.files); let archive = ZipArchive::from_finalized_writer(files, comment, inner, central_start)?;
Ok(archive)
}
}
impl<W: Write + Seek> ZipWriter<W> { /// Initializes the archive. /// /// Before writing to this object, the [`ZipWriter::start_file`] function should be called. /// After a successful write, the file remains open for writing. After a failed write, call /// [`ZipWriter::is_writing_file`] to determine if the file remains open. pubfn new(inner: W) -> ZipWriter<W> {
ZipWriter {
inner: Storer(MaybeEncrypted::Unencrypted(inner)),
files: IndexMap::new(),
stats: Default::default(),
writing_to_file: false,
writing_raw: false,
comment: Box::new([]),
flush_on_finish_file: false,
}
}
/// Returns true if a file is currently open for writing. pubconstfn is_writing_file(&self) -> bool { self.writing_to_file && !self.inner.is_closed()
}
/// Set ZIP archive comment. pubfn set_comment<S>(&mutself, comment: S) where
S: Into<Box<str>>,
{ self.set_raw_comment(comment.into().into_boxed_bytes())
}
/// Set ZIP archive comment. /// /// This sets the raw bytes of the comment. The comment /// is typically expected to be encoded in UTF-8. pubfn set_raw_comment(&mutself, comment: Box<[u8]>) { self.comment = comment;
}
/// Get ZIP archive comment. pubfn get_comment(&mutself) -> Result<&str, Utf8Error> {
from_utf8(self.get_raw_comment())
}
/// Get ZIP archive comment. /// /// This returns the raw bytes of the comment. The comment /// is typically expected to be encoded in UTF-8. pubconstfn get_raw_comment(&self) -> &[u8] {
&self.comment
}
/// Start a new file for with the requested options. fn start_entry<S, SToOwned, T: FileOptionExtension>(
&mutself,
name: S,
options: FileOptions<T>,
raw_values: Option<ZipRawValues>,
) -> ZipResult<()> where
S: Into<Box<str>> + ToOwned<Owned = SToOwned>,
SToOwned: Into<Box<str>>,
{ self.finish_file()?;
let extra_data = extra_field.get_or_insert_with(Default::default); let extra_data = match Arc::get_mut(extra_data) {
Some(exclusive) => exclusive,
None => { let new = Arc::new(extra_data.to_vec());
Arc::get_mut(extra_field.insert(new)).unwrap()
}
};
if extra_data.len() + AES_DUMMY_EXTRA_DATA.len() > u16::MAX as usize { let _ = self.abort_file(); return Err(InvalidArchive("Extra data field is too large"));
}
aes_extra_data_start = extra_data.len() as u64;
// We write zero bytes for now since we need to update the data when finishing the // file.
extra_data.write_all(&AES_DUMMY_EXTRA_DATA)?;
}
{ let header_start = self.inner.get_plain().stream_position()?;
letmut file = ZipFileData::initialize_local_block(
name,
&options,
raw_values,
header_start,
None,
aes_extra_data_start,
compression_method,
aes_mode,
extra_field,
);
file.version_made_by = file.version_made_by.max(file.version_needed() as u8); let index = self.insert_file_data(file)?; let file = &mutself.files[index]; let writer = self.inner.get_plain();
let block = match file.local_block() {
Ok(block) => block,
Err(e) => { let _ = self.abort_file(); return Err(e);
}
}; match block.write(writer) {
Ok(()) => (),
Err(e) => { let _ = self.abort_file(); return Err(e);
}
} // file name
writer.write_all(&file.file_name_raw)?; // zip64 extra field if file.large_file {
write_local_zip64_extra_field(writer, file)?;
} iflet Some(extra_field) = &file.extra_field {
file.extra_data_start = Some(writer.stream_position()?);
writer.write_all(extra_field)?;
} letmut header_end = writer.stream_position()?; if options.alignment > 1 { let align = options.alignment as u64; let unaligned_header_bytes = header_end % align; if unaligned_header_bytes != 0 { let pad_length = (align - unaligned_header_bytes) as usize; let Some(new_extra_field_length) =
(pad_length as u16).checked_add(block.extra_field_length) else { let _ = self.abort_file(); return Err(InvalidArchive( "Extra data field would be larger than allowed after aligning",
));
}; if pad_length >= 4 { // Add an extra field to the extra_data, per APPNOTE 4.6.11 letmut pad_body = vec![0; pad_length - 4]; if pad_body.len() >= 2 {
[pad_body[0], pad_body[1]] = options.alignment.to_le_bytes();
}
writer.write_u16_le(0xa11e)?;
writer
.write_u16_le(pad_body.len() as u16)
.map_err(ZipError::from)?;
writer.write_all(&pad_body).map_err(ZipError::from)?;
} else { // extra_data padding is too small for an extra field header, so pad with // zeroes let pad = vec![0; pad_length];
writer.write_all(&pad).map_err(ZipError::from)?;
}
header_end = writer.stream_position()?;
// Update extra field length in local file header.
writer.seek(SeekFrom::Start(file.header_start + 28))?;
writer.write_u16_le(new_extra_field_length)?;
writer.seek(SeekFrom::Start(header_end))?;
debug_assert_eq!(header_end % align, 0);
}
} match options.encrypt_with { #[cfg(feature = "aes-crypto")]
Some(EncryptWith::Aes { mode, password }) => { let aeswriter = AesWriter::new(
mem::replace(&mutself.inner, GenericZipWriter::Closed).unwrap(),
mode,
password.as_bytes(),
)?; self.inner = GenericZipWriter::Storer(MaybeEncrypted::Aes(aeswriter));
}
Some(EncryptWith::ZipCrypto(keys, ..)) => { letmut zipwriter = crate::zipcrypto::ZipCryptoWriter {
writer: mem::replace(&mutself.inner, Closed).unwrap(),
buffer: vec![],
keys,
}; let crypto_header = [0u8; 12];
file.crc32 = self.stats.hasher.clone().finalize(); iflet Some(aes_mode) = &mut file.aes_mode { // We prefer using AE-1 which provides an extra CRC check, but for small files we // switch to AE-2 to prevent being able to use the CRC value to to reconstruct the // unencrypted contents. // // C.f. https://www.winzip.com/en/support/aes-encryption/#crc-faq
aes_mode.1 = ifself.stats.bytes_written < 20 {
file.crc32 = 0;
AesVendorVersion::Ae2
} else {
AesVendorVersion::Ae1
}
}
/// Removes the file currently being written from the archive if there is one, or else removes /// the file most recently written. pubfn abort_file(&mutself) -> ZipResult<()> { let (_, last_file) = self.files.pop().ok_or(ZipError::FileNotFound)?; let make_plain_writer = self.inner.prepare_next_writer(
Stored,
None, #[cfg(feature = "deflate-zopfli")]
None,
)?; self.inner.switch_to(make_plain_writer)?; self.switch_to_non_encrypting_writer()?; // Make sure this is the last file, and that no shallow copies of it remain; otherwise we'd // overwrite a valid file and corrupt the archive let rewind_safe: bool = match last_file.data_start.get() {
None => self.files.is_empty(),
Some(last_file_start) => self.files.values().all(|file| {
file.data_start
.get()
.is_some_and(|start| start < last_file_start)
}),
}; if rewind_safe { self.inner
.get_plain()
.seek(SeekFrom::Start(last_file.header_start))?;
} self.writing_to_file = false;
Ok(())
}
/// Create a file in the archive and start writing its' contents. The file must not have the /// same name as a file already in the archive. /// /// The data should be written using the [`Write`] implementation on this [`ZipWriter`] pubfn start_file<S, T: FileOptionExtension, SToOwned>(
&mutself,
name: S, mut options: FileOptions<T>,
) -> ZipResult<()> where
S: Into<Box<str>> + ToOwned<Owned = SToOwned>,
SToOwned: Into<Box<str>>,
{ Self::normalize_options(&mut options); let make_new_self = self.inner.prepare_next_writer(
options.compression_method,
options.compression_level, #[cfg(feature = "deflate-zopfli")]
options.zopfli_buffer_size,
)?; self.start_entry(name, options, None)?; iflet Err(e) = self.inner.switch_to(make_new_self) { self.abort_file().unwrap(); return Err(e);
} self.writing_raw = false;
Ok(())
}
/* TODO: link to/use Self::finish_into_readable() from https://github.com/zip-rs/zip/pull/400 in
* this docstring. */ /// Copy over the entire contents of another archive verbatim. /// /// This method extracts file metadata from the `source` archive, then simply performs a single /// big [`io::copy()`](io::copy) to transfer all the actual file contents without any /// decompression or decryption. This is more performant than the equivalent operation of /// calling [`Self::raw_copy_file()`] for each entry from the `source` archive in sequence. /// ///``` /// # fn main() -> Result<(), zip::result::ZipError> { /// use std::io::{Cursor, prelude::*}; /// use zip::{ZipArchive, ZipWriter, write::SimpleFileOptions}; /// /// let buf = Cursor::new(Vec::new()); /// let mut zip = ZipWriter::new(buf); /// zip.start_file("a.txt", SimpleFileOptions::default())?; /// zip.write_all(b"hello\n")?; /// let src = ZipArchive::new(zip.finish()?)?; /// /// let buf = Cursor::new(Vec::new()); /// let mut zip = ZipWriter::new(buf); /// zip.start_file("b.txt", SimpleFileOptions::default())?; /// zip.write_all(b"hey\n")?; /// let src2 = ZipArchive::new(zip.finish()?)?; /// /// let buf = Cursor::new(Vec::new()); /// let mut zip = ZipWriter::new(buf); /// zip.merge_archive(src)?; /// zip.merge_archive(src2)?; /// let mut result = ZipArchive::new(zip.finish()?)?; /// /// let mut s: String = String::new(); /// result.by_name("a.txt")?.read_to_string(&mut s)?; /// assert_eq!(s, "hello\n"); /// s.clear(); /// result.by_name("b.txt")?.read_to_string(&mut s)?; /// assert_eq!(s, "hey\n"); /// # Ok(()) /// # } ///``` pubfn merge_archive<R>(&mutself, mut source: ZipArchive<R>) -> ZipResult<()> where
R: Read + io::Seek,
{ self.finish_file()?;
/* Ensure we accept the file contents on faith (and avoid overwriting the data).
* See raw_copy_file_rename(). */ self.writing_to_file = true; self.writing_raw = true;
let writer = self.inner.get_plain(); /* Get the file entries from the source archive. */ let new_files = source.merge_contents(writer)?;
/* These file entries are now ours! */ self.files.extend(new_files);
/// Starts a file, taking a Path as argument. /// /// This function ensures that the '/' path separator is used and normalizes `.` and `..`. It /// ignores any `..` or Windows drive letter that would produce a path outside the ZIP file's /// root. pubfn start_file_from_path<E: FileOptionExtension, P: AsRef<Path>>(
&mutself,
path: P,
options: FileOptions<E>,
) -> ZipResult<()> { self.start_file(path_to_string(path), options)
}
/// 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(()) /// } /// ``` pubfn raw_copy_file_rename<S, SToOwned>(&mutself, mut file: ZipFile, name: S) -> ZipResult<()> where
S: Into<Box<str>> + ToOwned<Owned = SToOwned>,
SToOwned: Into<Box<str>>,
{ letmut options = SimpleFileOptions::default()
.large_file(file.compressed_size().max(file.size()) > spec::ZIP64_BYTES_THR)
.last_modified_time(
file.last_modified()
.unwrap_or_else(DateTime::default_for_write),
)
.compression_method(file.compression()); iflet Some(perms) = file.unix_mode() {
options = options.unix_permissions(perms);
} Self::normalize_options(&mut options);
/// Like `raw_copy_file_to_path`, but uses Path arguments. /// /// This function ensures that the '/' path separator is used and normalizes `.` and `..`. It /// ignores any `..` or Windows drive letter that would produce a path outside the ZIP file's /// root. pubfn raw_copy_file_to_path<P: AsRef<Path>>(
&mutself,
file: ZipFile,
path: P,
) -> ZipResult<()> { self.raw_copy_file_rename(file, path_to_string(path))
}
/// 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(()) /// } /// ``` pubfn raw_copy_file(&mutself, file: ZipFile) -> ZipResult<()> { let name = file.name().to_owned(); self.raw_copy_file_rename(file, name)
}
/// Add a directory entry. /// /// As directories have no content, you must not call [`ZipWriter::write`] before adding a new file. pubfn add_directory<S, T: FileOptionExtension>(
&mutself,
name: S, mut options: FileOptions<T>,
) -> ZipResult<()> where
S: Into<String>,
{ if options.permissions.is_none() {
options.permissions = Some(0o755);
}
*options.permissions.as_mut().unwrap() |= 0o40000;
options.compression_method = Stored;
options.encrypt_with = None;
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 + "/",
};
/// Add a directory entry, taking a Path as argument. /// /// This function ensures that the '/' path separator is used and normalizes `.` and `..`. It /// ignores any `..` or Windows drive letter that would produce a path outside the ZIP file's /// root. pubfn add_directory_from_path<T: FileOptionExtension, P: AsRef<Path>>(
&mutself,
path: P,
options: FileOptions<T>,
) -> 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. pubfn finish(mutself) -> ZipResult<W> { let _central_start = self.finalize()?; let inner = mem::replace(&mutself.inner, 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. pubfn add_symlink<N, NToOwned, T, E: FileOptionExtension>(
&mutself,
name: N,
target: T, mut options: FileOptions<E>,
) -> ZipResult<()> where
N: Into<Box<str>> + ToOwned<Owned = NToOwned>,
NToOwned: Into<Box<str>>,
T: Into<Box<str>>,
{ if options.permissions.is_none() {
options.permissions = Some(0o777);
}
*options.permissions.as_mut().unwrap() |= S_IFLNK; // The symlink target is stored as file content. And compressing the target path // likely wastes space. So always store.
options.compression_method = Stored;
/// Add a symlink entry, taking Paths to the location and target as arguments. /// /// This function ensures that the '/' path separator is used and normalizes `.` and `..`. It /// ignores any `..` or Windows drive letter that would produce a path outside the ZIP file's /// root. pubfn add_symlink_from_path<P: AsRef<Path>, T: AsRef<Path>, E: FileOptionExtension>(
&mutself,
path: P,
target: T,
options: FileOptions<E>,
) -> ZipResult<()> { self.add_symlink(path_to_string(path), path_to_string(target), options)
}
let central_start = { let central_start = self.write_central_and_footer()?; let writer = self.inner.get_plain(); let footer_end = writer.stream_position()?; let file_end = writer.seek(SeekFrom::End(0))?; if footer_end < file_end { // Data from an aborted file is past the end of the footer, so rewrite the footer at // the actual end. let central_and_footer_size = footer_end - central_start;
writer.seek(SeekFrom::End(-(central_and_footer_size as i64)))?; self.write_central_and_footer()?;
}
central_start
};
Ok(central_start)
}
fn write_central_and_footer(&mutself) -> Result<u64, ZipError> { let writer = self.inner.get_plain();
letmut version_needed = MIN_VERSION as u16; let central_start = writer.stream_position()?; for file inself.files.values() {
write_central_directory_header(writer, file)?;
version_needed = version_needed.max(file.version_needed());
} let central_size = writer.stream_position()? - central_start;
/// Adds another entry to the central directory referring to the same content as an existing /// entry. The file's local-file header will still refer to it by its original name, so /// unzipping the file will technically be unspecified behavior. [ZipArchive] ignores the /// filename in the local-file header and treat the central directory as authoritative. However, /// some other software (e.g. Minecraft) will refuse to extract a file copied this way. pubfn shallow_copy_file(&mutself, src_name: &str, dest_name: &str) -> ZipResult<()> { self.finish_file()?; let src_index = self.index_by_name(src_name)?; letmut dest_data = self.files[src_index].to_owned();
dest_data.file_name = dest_name.to_string().into();
dest_data.file_name_raw = dest_name.to_string().into_bytes().into(); self.insert_file_data(dest_data)?;
Ok(())
}
/// Like `shallow_copy_file`, but uses Path arguments. /// /// This function ensures that the '/' path separator is used and normalizes `.` and `..`. It /// ignores any `..` or Windows drive letter that would produce a path outside the ZIP file's /// root. pubfn shallow_copy_file_from_path<T: AsRef<Path>, U: AsRef<Path>>(
&mutself,
src_path: T,
dest_path: U,
) -> ZipResult<()> { self.shallow_copy_file(&path_to_string(src_path), &path_to_string(dest_path))
}
}
impl<W: Write + Seek> Drop for ZipWriter<W> { fn drop(&mutself) { if !self.inner.is_closed() { iflet Err(e) = self.finalize() { let _ = write!(io::stderr(), "ZipWriter drop failed: {:?}", e);
}
}
}
}
type SwitchWriterFunction<W> = Box<dyn FnOnce(MaybeEncrypted<W>) -> GenericZipWriter<W>>;
fn get_plain(&mutself) -> &mut W { match *self {
Storer(MaybeEncrypted::Unencrypted(refmut w)) => w,
_ => panic!("Should have switched to stored and unencrypted beforehand"),
}
}
fn unwrap(self) -> W { matchself {
Storer(MaybeEncrypted::Unencrypted(w)) => w,
_ => panic!("Should have switched to stored and unencrypted beforehand"),
}
}
}
#[cfg(feature = "_deflate-any")] fn deflate_compression_level_range() -> std::ops::RangeInclusive<i64> { let min = if cfg!(feature = "deflate-flate2") {
Compression::fast().level() as i64
} else {
Compression::best().level() as i64 + 1
};
let max = Compression::best().level() as i64
+ if cfg!(feature = "deflate-zopfli") {
u8::MAX as i64
} else { 0
};
min..=max
}
#[cfg(feature = "bzip2")] fn bzip2_compression_level_range() -> std::ops::RangeInclusive<i64> { let min = bzip2::Compression::fast().level() as i64; let max = bzip2::Compression::best().level() as i64;
min..=max
}
/* TODO: implement this using the Block trait! */ // Extra field header ID.
buf.write_u16_le(0x9901)?; // Data size.
buf.write_u16_le(7)?; // Integer version number.
buf.write_u16_le(version as u16)?; // Vendor ID.
buf.write_all(b"AE")?; // AES encryption strength.
buf.write_all(&[aes_mode as u8])?; // Real compression method.
buf.write_u16_le(compression_method.serialize_to_u16())?;
writer.write_all(&buf)?;
let aes_extra_data_start = file.aes_extra_data_start as usize; let extra_field = Arc::get_mut(file.extra_field.as_mut().unwrap()).unwrap();
extra_field
.splice(
aes_extra_data_start..(aes_extra_data_start + buf.len()),
buf,
)
.count();
Ok(())
}
fn update_local_file_header<T: Write + Seek>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> { const CRC32_OFFSET: u64 = 14;
writer.seek(SeekFrom::Start(file.header_start + CRC32_OFFSET))?;
writer.write_u32_le(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_le(file.compressed_size as u32)?; // uncompressed size is already checked on write to catch it as soon as possible
writer.write_u32_le(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 letmut zip64_extra_field = [0; 28]; let zip64_extra_field_length =
write_central_zip64_extra_field(&mut zip64_extra_field.as_mut(), file)?; let block = file.block(zip64_extra_field_length);
block.write(writer)?; // file name
writer.write_all(&file.file_name_raw)?; // zip64 extra field
writer.write_all(&zip64_extra_field[..zip64_extra_field_length as usize])?; // extra field iflet Some(extra_field) = &file.extra_field {
writer.write_all(extra_field)?;
} iflet Some(central_extra_field) = &file.central_extra_field {
writer.write_all(central_extra_field)?;
} // file comment
writer.write_all(file.file_comment.as_bytes())?;
Ok(())
}
fn validate_extra_data(header_id: u16, data: &[u8]) -> ZipResult<()> { if data.len() > u16::MAX as usize { return Err(ZipError::Io(io::Error::new(
io::ErrorKind::Other, "Extra-data field can't exceed u16::MAX bytes",
)));
} if header_id == 0x0001 { return Err(ZipError::Io(io::Error::new(
io::ErrorKind::Other, "No custom ZIP64 extra data allowed",
)));
}
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. let Some(block) = file.zip64_extra_field_block() else { return Err(ZipError::InvalidArchive( "Attempted to write a ZIP64 extra field for a file that's within zip32 limits",
));
}; let block = block.serialize();
writer.write_all(&block)?;
Ok(())
}
fn update_local_zip64_extra_field<T: Write + Seek>(
writer: &mut T,
file: &ZipFileData,
) -> ZipResult<()> { if !file.large_file { return Err(ZipError::InvalidArchive( "Attempted to update a nonexistent ZIP64 extra field",
));
}
let zip64_extra_field = file.header_start
+ mem::size_of::<ZipLocalEntryBlock>() as u64
+ file.file_name_raw.len() as u64;
writer.seek(SeekFrom::Start(zip64_extra_field))?;
let block = file.zip64_extra_field_block().unwrap(); let block = block.serialize();
writer.write_all(&block)?;
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. match file.zip64_extra_field_block() {
None => Ok(0),
Some(block) => { let block = block.serialize();
writer.write_all(&block)?; let len: u16 = block.len().try_into().unwrap();
Ok(len)
}
}
}
#[cfg(test)] mod test { usesuper::{FileOptions, ZipWriter}; usecrate::compression::CompressionMethod; usecrate::result::ZipResult; usecrate::types::DateTime; usecrate::write::SimpleFileOptions; usecrate::CompressionMethod::Stored; usecrate::ZipArchive; use std::io; use std::io::{Cursor, Read, Write}; use std::path::PathBuf;
assert_eq!(result.get_ref().len(), 153); letmut v = Vec::new();
v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip"));
assert_eq!(result.get_ref(), &v);
}
const RT_TEST_TEXT: &str = "And I can't stop thinking about the moments that I lost to you\
And I can't stop thinking of things I used to do\
And I can't stop making bad decisions\
And I can't stop eating stuff you make me chew\
I put on a smile like you wanna see\
Another day goes by that I long to be like you"; const RT_TEST_FILENAME: &str = "subfolder/sub-subfolder/can't_stop.txt"; const SECOND_FILENAME: &str = "different_name.xyz"; const THIRD_FILENAME: &str = "third_name.xyz";
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.