diff options
author | Joel Galenson <jgalenson@google.com> | 2021-06-21 14:24:55 -0700 |
---|---|---|
committer | Joel Galenson <jgalenson@google.com> | 2021-06-21 14:24:55 -0700 |
commit | 4558110cf1723a139cbfaa23f42388c0b435855c (patch) | |
tree | c656a09ca55d8be817ce207710747a03abebdaed | |
parent | 2f084162167e8953d07ff7ba17b999b737a9e05a (diff) | |
download | zip-4558110cf1723a139cbfaa23f42388c0b435855c.tar.gz |
Upgrade rust/crates/zip to 0.5.13
Test: make
Change-Id: I88a94ddcb8db0f4b7196117a9e08832a1e5b99ca
-rw-r--r-- | .cargo_vcs_info.json | 2 | ||||
-rw-r--r-- | .github/workflows/ci.yaml | 2 | ||||
-rw-r--r-- | Android.bp | 17 | ||||
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | Cargo.toml.orig | 5 | ||||
-rw-r--r-- | METADATA | 8 | ||||
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | src/read.rs | 77 | ||||
-rw-r--r-- | src/result.rs | 15 | ||||
-rw-r--r-- | src/spec.rs | 22 | ||||
-rw-r--r-- | src/types.rs | 21 | ||||
-rw-r--r-- | src/write.rs | 531 | ||||
-rw-r--r-- | src/zipcrypto.rs | 36 | ||||
-rw-r--r-- | tests/end_to_end.rs | 49 | ||||
-rw-r--r-- | tests/zip_crypto.rs | 6 |
15 files changed, 698 insertions, 100 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json index 935452f..b1502c1 100644 --- a/.cargo_vcs_info.json +++ b/.cargo_vcs_info.json @@ -1,5 +1,5 @@ { "git": { - "sha1": "88e6f87884a906ee63962d3a2cf87f890878a46d" + "sha1": "7edf2489d5cff8b80f02ee6fc5febf3efd0a9442" } } diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1e7b395..8374189 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macOS-latest, windows-latest] - rust: [stable, 1.34.0] + rust: [stable, 1.36.0] steps: - uses: actions/checkout@master @@ -38,17 +38,16 @@ rust_library { // dependent_library ["feature_list"] // byteorder-1.4.3 "default,std" -// cc-1.0.67 -// cfg-if-0.1.10 +// cc-1.0.68 // cfg-if-1.0.0 // crc32fast-1.2.1 "default,std" -// flate2-1.0.14 "any_zlib,libz-sys,zlib" -// libc-0.2.94 "default,std" -// libz-sys-1.1.3 "default,libc,stock-zlib" +// flate2-1.0.20 "any_zlib,libz-sys,zlib" +// libc-0.2.97 "default,std" +// libz-sys-1.1.3 // pkg-config-0.3.19 -// proc-macro2-1.0.26 "default,proc-macro" +// proc-macro2-1.0.27 "default,proc-macro" // quote-1.0.9 "default,proc-macro" -// syn-1.0.72 "clone-impls,default,derive,parsing,printing,proc-macro,quote" -// thiserror-1.0.24 -// thiserror-impl-1.0.24 +// syn-1.0.73 "clone-impls,default,derive,parsing,printing,proc-macro,quote" +// thiserror-1.0.25 +// thiserror-impl-1.0.25 // unicode-xid-0.2.2 "default" @@ -13,7 +13,7 @@ [package] edition = "2018" name = "zip" -version = "0.5.12" +version = "0.5.13" authors = ["Mathijs van de Nes <git@mathijs.vd-nes.nl>", "Marli Frost <marli@frost.red>", "Ryan Levick <ryan.levick@gmail.com>"] description = "Library to support the reading and writing of zip files.\n" keywords = ["zip", "archive"] @@ -27,7 +27,7 @@ harness = false version = "1.3" [dependencies.bzip2] -version = "0.3" +version = "0.4" optional = true [dependencies.crc32fast] @@ -58,3 +58,4 @@ default = ["bzip2", "deflate", "time"] deflate = ["flate2/rust_backend"] deflate-miniz = ["flate2/default"] deflate-zlib = ["flate2/zlib"] +unreserved = [] diff --git a/Cargo.toml.orig b/Cargo.toml.orig index b7e0fb8..2ce7e60 100644 --- a/Cargo.toml.orig +++ b/Cargo.toml.orig @@ -1,6 +1,6 @@ [package] name = "zip" -version = "0.5.12" +version = "0.5.13" authors = ["Mathijs van de Nes <git@mathijs.vd-nes.nl>", "Marli Frost <marli@frost.red>", "Ryan Levick <ryan.levick@gmail.com>"] license = "MIT" repository = "https://github.com/zip-rs/zip.git" @@ -14,7 +14,7 @@ edition = "2018" flate2 = { version = "1.0.0", default-features = false, optional = true } time = { version = "0.1", optional = true } byteorder = "1.3" -bzip2 = { version = "0.3", optional = true } +bzip2 = { version = "0.4", optional = true } crc32fast = "1.0" thiserror = "1.0" @@ -27,6 +27,7 @@ walkdir = "2" deflate = ["flate2/rust_backend"] deflate-miniz = ["flate2/default"] deflate-zlib = ["flate2/zlib"] +unreserved = [] default = ["bzip2", "deflate", "time"] [[bench]] @@ -7,13 +7,13 @@ third_party { } url { type: ARCHIVE - value: "https://static.crates.io/crates/zip/zip-0.5.12.crate" + value: "https://static.crates.io/crates/zip/zip-0.5.13.crate" } - version: "0.5.12" + version: "0.5.13" license_type: NOTICE last_upgrade_date { year: 2021 - month: 5 - day: 19 + month: 6 + day: 21 } } @@ -4,7 +4,7 @@ zip-rs [![Build Status](https://img.shields.io/github/workflow/status/zip-rs/zip/CI)](https://github.com/zip-rs/zip/actions?query=branch%3Amaster+workflow%3ACI) [![Crates.io version](https://img.shields.io/crates/v/zip.svg)](https://crates.io/crates/zip) -[Documentation](https://docs.rs/zip/0.5.10/zip/) +[Documentation](https://docs.rs/zip/0.5.13/zip/) Info diff --git a/src/read.rs b/src/read.rs index 3aac00f..97bccd2 100644 --- a/src/read.rs +++ b/src/read.rs @@ -4,8 +4,7 @@ use crate::compression::CompressionMethod; use crate::crc32::Crc32Reader; use crate::result::{InvalidPassword, ZipError, ZipResult}; use crate::spec; -use crate::zipcrypto::ZipCryptoReader; -use crate::zipcrypto::ZipCryptoReaderValid; +use crate::zipcrypto::{ZipCryptoReader, ZipCryptoReaderValid, ZipCryptoValidator}; use std::borrow::Cow; use std::collections::HashMap; use std::io::{self, prelude::*}; @@ -47,7 +46,7 @@ mod ffi { /// } /// ``` #[derive(Clone, Debug)] -pub struct ZipArchive<R: Read + io::Seek> { +pub struct ZipArchive<R> { reader: R, files: Vec<ZipFileData>, names_map: HashMap<String, usize>, @@ -161,6 +160,8 @@ fn find_content<'a>( fn make_crypto_reader<'a>( compression_method: crate::compression::CompressionMethod, crc32: u32, + last_modified_time: DateTime, + using_data_descriptor: bool, reader: io::Take<&'a mut dyn io::Read>, password: Option<&[u8]>, ) -> ZipResult<Result<CryptoReader<'a>, InvalidPassword>> { @@ -173,10 +174,17 @@ fn make_crypto_reader<'a>( let reader = match password { None => CryptoReader::Plaintext(reader), - Some(password) => match ZipCryptoReader::new(reader, password).validate(crc32)? { - None => return Ok(Err(InvalidPassword)), - Some(r) => CryptoReader::ZipCrypto(r), - }, + Some(password) => { + let validator = if using_data_descriptor { + ZipCryptoValidator::InfoZipMsdosTime(last_modified_time.timepart()) + } else { + ZipCryptoValidator::PkzipCrc32(crc32) + }; + match ZipCryptoReader::new(reader, password).validate(validator)? { + None => return Ok(Err(InvalidPassword)), + Some(r) => CryptoReader::ZipCrypto(r), + } + } }; Ok(Ok(reader)) } @@ -209,7 +217,7 @@ fn make_reader<'a>( impl<R: Read + io::Seek> ZipArchive<R> { /// Get the directory start offset and number of files. This is done in a /// separate function to ease the control flow design. - fn get_directory_counts( + pub(crate) fn get_directory_counts( reader: &mut R, footer: &spec::CentralDirectoryEnd, cde_start_pos: u64, @@ -481,17 +489,20 @@ impl<R: Read + io::Seek> ZipArchive<R> { let data = &mut self.files[file_number]; match (password, data.encrypted) { - (None, true) => { - return Err(ZipError::UnsupportedArchive( - "Password required to decrypt file", - )) - } + (None, true) => return Err(ZipError::UnsupportedArchive(ZipError::PASSWORD_REQUIRED)), (Some(_), false) => password = None, //Password supplied, but none needed! Discard. _ => {} } let limit_reader = find_content(data, &mut self.reader)?; - match make_crypto_reader(data.compression_method, data.crc32, limit_reader, password) { + match make_crypto_reader( + data.compression_method, + data.crc32, + data.last_modified_time, + data.using_data_descriptor, + limit_reader, + password, + ) { Ok(Ok(crypto_reader)) => Ok(Ok(ZipFile { crypto_reader: Some(crypto_reader), reader: ZipFileReader::NoReader, @@ -514,7 +525,8 @@ fn unsupported_zip_error<T>(detail: &'static str) -> ZipResult<T> { Err(ZipError::UnsupportedArchive(detail)) } -fn central_header_to_zip_file<R: Read + io::Seek>( +/// Parse a central directory entry to collect the information for the file. +pub(crate) fn central_header_to_zip_file<R: Read + io::Seek>( reader: &mut R, archive_offset: u64, ) -> ZipResult<ZipFileData> { @@ -530,6 +542,7 @@ fn central_header_to_zip_file<R: Read + io::Seek>( let flags = reader.read_u16::<LittleEndian>()?; let encrypted = flags & 1 == 1; let is_utf8 = flags & (1 << 11) != 0; + let using_data_descriptor = flags & (1 << 3) != 0; let compression_method = reader.read_u16::<LittleEndian>()?; let last_mod_time = reader.read_u16::<LittleEndian>()?; let last_mod_date = reader.read_u16::<LittleEndian>()?; @@ -564,6 +577,7 @@ fn central_header_to_zip_file<R: Read + io::Seek>( system: System::from_u8((version_made_by >> 8) as u8), version_made_by: version_made_by as u8, encrypted, + using_data_descriptor, compression_method: { #[allow(deprecated)] CompressionMethod::from_u16(compression_method) @@ -574,14 +588,16 @@ fn central_header_to_zip_file<R: Read + io::Seek>( uncompressed_size: uncompressed_size as u64, file_name, file_name_raw, + extra_field, file_comment, header_start: offset, central_header_start, data_start: 0, external_attributes: external_file_attributes, + large_file: false, }; - match parse_extra_field(&mut result, &*extra_field) { + match parse_extra_field(&mut result) { Ok(..) | Err(ZipError::Io(..)) => {} Err(e) => return Err(e), } @@ -592,20 +608,22 @@ fn central_header_to_zip_file<R: Read + io::Seek>( Ok(result) } -fn parse_extra_field(file: &mut ZipFileData, data: &[u8]) -> ZipResult<()> { - let mut reader = io::Cursor::new(data); +fn parse_extra_field(file: &mut ZipFileData) -> ZipResult<()> { + let mut reader = io::Cursor::new(&file.extra_field); - while (reader.position() as usize) < data.len() { + while (reader.position() as usize) < file.extra_field.len() { let kind = reader.read_u16::<LittleEndian>()?; let len = reader.read_u16::<LittleEndian>()?; let mut len_left = len as i64; // Zip64 extended information extra field if kind == 0x0001 { if file.uncompressed_size == 0xFFFFFFFF { + file.large_file = true; file.uncompressed_size = reader.read_u64::<LittleEndian>()?; len_left -= 8; } if file.compressed_size == 0xFFFFFFFF { + file.large_file = true; file.compressed_size = reader.read_u64::<LittleEndian>()?; len_left -= 8; } @@ -797,6 +815,11 @@ impl<'a> ZipFile<'a> { self.data.crc32 } + /// Get the extra data of the zip header for this file + pub fn extra_data(&self) -> &[u8] { + &self.data.extra_field + } + /// Get the starting offset of the data of the compressed file pub fn data_start(&self) -> u64 { self.data.data_start @@ -907,6 +930,7 @@ pub fn read_zipfile_from_stream<'a, R: io::Read>( system: System::from_u8((version_made_by >> 8) as u8), version_made_by: version_made_by as u8, encrypted, + using_data_descriptor, compression_method, last_modified_time: DateTime::from_msdos(last_mod_date, last_mod_time), crc32, @@ -914,6 +938,7 @@ pub fn read_zipfile_from_stream<'a, R: io::Read>( uncompressed_size: uncompressed_size as u64, file_name, file_name_raw, + extra_field, file_comment: String::new(), // file comment is only available in the central directory // header_start and data start are not available, but also don't matter, since seeking is // not available. @@ -924,9 +949,10 @@ pub fn read_zipfile_from_stream<'a, R: io::Read>( // We set this to zero, which should be valid as the docs state 'If input came // from standard input, this field is set to zero.' external_attributes: 0, + large_file: false, }; - match parse_extra_field(&mut result, &extra_field) { + match parse_extra_field(&mut result) { Ok(..) | Err(ZipError::Io(..)) => {} Err(e) => return Err(e), } @@ -942,8 +968,15 @@ pub fn read_zipfile_from_stream<'a, R: io::Read>( let result_crc32 = result.crc32; let result_compression_method = result.compression_method; - let crypto_reader = - make_crypto_reader(result_compression_method, result_crc32, limit_reader, None)?.unwrap(); + let crypto_reader = make_crypto_reader( + result_compression_method, + result_crc32, + result.last_modified_time, + result.using_data_descriptor, + limit_reader, + None, + )? + .unwrap(); Ok(Some(ZipFile { data: Cow::Owned(result), diff --git a/src/result.rs b/src/result.rs index e8b7d05..5d5ab45 100644 --- a/src/result.rs +++ b/src/result.rs @@ -32,6 +32,21 @@ pub enum ZipError { FileNotFound, } +impl ZipError { + /// The text used as an error when a password is required and not supplied + /// + /// ```rust,no_run + /// # use zip::result::ZipError; + /// # let mut archive = zip::ZipArchive::new(std::io::Cursor::new(&[])).unwrap(); + /// match archive.by_index(1) { + /// Err(ZipError::UnsupportedArchive(ZipError::PASSWORD_REQUIRED)) => eprintln!("a password is needed to unzip this file"), + /// _ => (), + /// } + /// # () + /// ``` + pub const PASSWORD_REQUIRED: &'static str = "Password required to decrypt file"; +} + impl From<ZipError> for io::Error { fn from(err: ZipError) -> io::Error { io::Error::new(io::ErrorKind::Other, err) diff --git a/src/spec.rs b/src/spec.rs index 91966b6..4ab3656 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -117,6 +117,14 @@ impl Zip64CentralDirectoryEndLocator { number_of_disks, }) } + + pub fn write<T: Write>(&self, writer: &mut T) -> ZipResult<()> { + writer.write_u32::<LittleEndian>(ZIP64_CENTRAL_DIRECTORY_END_LOCATOR_SIGNATURE)?; + writer.write_u32::<LittleEndian>(self.disk_with_central_directory)?; + writer.write_u64::<LittleEndian>(self.end_of_central_directory_offset)?; + writer.write_u32::<LittleEndian>(self.number_of_disks)?; + Ok(()) + } } pub struct Zip64CentralDirectoryEnd { @@ -179,4 +187,18 @@ impl Zip64CentralDirectoryEnd { "Could not find ZIP64 central directory end", )) } + + pub fn write<T: Write>(&self, writer: &mut T) -> ZipResult<()> { + writer.write_u32::<LittleEndian>(ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE)?; + writer.write_u64::<LittleEndian>(44)?; // record size + writer.write_u16::<LittleEndian>(self.version_made_by)?; + writer.write_u16::<LittleEndian>(self.version_needed_to_extract)?; + writer.write_u32::<LittleEndian>(self.disk_number)?; + writer.write_u32::<LittleEndian>(self.disk_with_central_directory)?; + writer.write_u64::<LittleEndian>(self.number_of_files_on_this_disk)?; + writer.write_u64::<LittleEndian>(self.number_of_files)?; + writer.write_u64::<LittleEndian>(self.central_directory_size)?; + writer.write_u64::<LittleEndian>(self.central_directory_offset)?; + Ok(()) + } } diff --git a/src/types.rs b/src/types.rs index 154e3c2..026aa15 100644 --- a/src/types.rs +++ b/src/types.rs @@ -216,6 +216,8 @@ pub struct ZipFileData { pub version_made_by: u8, /// True if the file is encrypted. pub encrypted: bool, + /// True if the file uses a data-descriptor section + pub using_data_descriptor: bool, /// Compression method used to store the file pub compression_method: crate::compression::CompressionMethod, /// Last modified time. This will only have a 2 second precision. @@ -230,6 +232,8 @@ pub struct ZipFileData { pub file_name: String, /// Raw file name. To be used when file_name was incorrectly decoded. pub file_name_raw: Vec<u8>, + /// Extra field usually used for storage expansion + pub extra_field: Vec<u8>, /// File comment pub file_comment: String, /// Specifies where the local header of the file starts @@ -242,6 +246,8 @@ pub struct ZipFileData { pub data_start: u64, /// External file attributes pub external_attributes: u32, + /// Reserve local ZIP64 extra field + pub large_file: bool, } impl ZipFileData { @@ -275,10 +281,18 @@ impl ZipFileData { }) } + pub fn zip64_extension(&self) -> bool { + self.uncompressed_size > 0xFFFFFFFF + || self.compressed_size > 0xFFFFFFFF + || self.header_start > 0xFFFFFFFF + } + pub fn version_needed(&self) -> u16 { - match self.compression_method { + // higher versions matched first + match (self.zip64_extension(), self.compression_method) { #[cfg(feature = "bzip2")] - crate::compression::CompressionMethod::Bzip2 => 46, + (_, crate::compression::CompressionMethod::Bzip2) => 46, + (true, _) => 45, _ => 20, } } @@ -303,6 +317,7 @@ mod test { system: System::Dos, version_made_by: 0, encrypted: false, + using_data_descriptor: false, compression_method: crate::compression::CompressionMethod::Stored, last_modified_time: DateTime::default(), crc32: 0, @@ -310,11 +325,13 @@ mod test { uncompressed_size: 0, file_name: file_name.clone(), file_name_raw: file_name.into_bytes(), + extra_field: Vec::new(), file_comment: String::new(), header_start: 0, data_start: 0, central_header_start: 0, external_attributes: 0, + large_file: false, }; assert_eq!( data.file_name_sanitized(), diff --git a/src/write.rs b/src/write.rs index bc68817..05c3666 100644 --- a/src/write.rs +++ b/src/write.rs @@ -1,11 +1,11 @@ //! Types for creating ZIP archives use crate::compression::CompressionMethod; -use crate::read::ZipFile; +use crate::read::{central_header_to_zip_file, ZipArchive, ZipFile}; use crate::result::{ZipError, ZipResult}; use crate::spec; use crate::types::{DateTime, System, ZipFileData, DEFAULT_VERSION}; -use byteorder::{LittleEndian, WriteBytesExt}; +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use crc32fast::Hasher; use std::default::Default; use std::io; @@ -68,8 +68,10 @@ pub struct ZipWriter<W: Write + io::Seek> { files: Vec<ZipFileData>, stats: ZipWriterStats, writing_to_file: bool, - comment: String, + writing_to_extra_field: bool, + writing_to_central_extra_field_only: bool, writing_raw: bool, + comment: Vec<u8>, } #[derive(Default)] @@ -91,6 +93,7 @@ pub struct FileOptions { compression_method: CompressionMethod, last_modified_time: DateTime, permissions: Option<u32>, + large_file: bool, } impl FileOptions { @@ -114,6 +117,7 @@ impl FileOptions { #[cfg(not(feature = "time"))] last_modified_time: DateTime::default(), permissions: None, + large_file: false, } } @@ -121,7 +125,6 @@ impl FileOptions { /// /// The default is `CompressionMethod::Deflated`. If the deflate compression feature is /// disabled, `CompressionMethod::Stored` becomes the default. - /// otherwise. pub fn compression_method(mut self, method: CompressionMethod) -> FileOptions { self.compression_method = method; self @@ -145,6 +148,16 @@ impl 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`. + pub fn large_file(mut self, large: bool) -> FileOptions { + self.large_file = large; + self + } } impl Default for FileOptions { @@ -163,11 +176,24 @@ impl<W: Write + io::Seek> Write for ZipWriter<W> { } match self.inner.ref_mut() { Some(ref mut w) => { - let write_result = w.write(buf); - if let Ok(count) = write_result { - self.stats.update(&buf[0..count]); + 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 > 0xFFFFFFFF + && !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 } - write_result } None => Err(io::Error::new( io::ErrorKind::BrokenPipe, @@ -194,6 +220,45 @@ impl ZipWriterStats { } } +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 let Err(_) = readwriter.seek(io::SeekFrom::Start(directory_start)) { + 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. /// @@ -204,8 +269,10 @@ impl<W: Write + io::Seek> ZipWriter<W> { files: Vec::new(), stats: Default::default(), writing_to_file: false, - comment: String::new(), + writing_to_extra_field: false, + writing_to_central_extra_field_only: false, writing_raw: false, + comment: Vec::new(), } } @@ -214,7 +281,15 @@ impl<W: Write + io::Seek> ZipWriter<W> { where S: Into<String>, { - self.comment = comment.into(); + 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. @@ -229,7 +304,6 @@ impl<W: Write + io::Seek> ZipWriter<W> { { self.finish_file()?; - let is_raw = raw_values.is_some(); let raw_values = raw_values.unwrap_or_else(|| ZipRawValues { crc32: 0, compressed_size: 0, @@ -245,6 +319,7 @@ impl<W: Write + io::Seek> ZipWriter<W> { system: System::Unix, version_made_by: DEFAULT_VERSION, encrypted: false, + using_data_descriptor: false, compression_method: options.compression_method, last_modified_time: options.last_modified_time, crc32: raw_values.crc32, @@ -252,11 +327,13 @@ impl<W: Write + io::Seek> ZipWriter<W> { 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: 0, central_header_start: 0, external_attributes: permissions << 16, + large_file: options.large_file, }; write_local_file_header(writer, &file)?; @@ -270,17 +347,14 @@ impl<W: Write + io::Seek> ZipWriter<W> { self.files.push(file); } - self.writing_raw = is_raw; - self.inner.switch_to(if is_raw { - CompressionMethod::Stored - } else { - options.compression_method - })?; - 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)?; let writer = self.inner.get_plain(); @@ -316,13 +390,14 @@ impl<W: Write + io::Seek> ZipWriter<W> { } *options.permissions.as_mut().unwrap() |= 0o100000; self.start_entry(name, options, None)?; + self.inner.switch_to(options.compression_method)?; self.writing_to_file = true; Ok(()) } /// Starts a file, taking a Path as argument. /// - /// This function ensures that the '/' path seperator is used. It also ignores all non 'Normal' + /// 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", @@ -336,6 +411,168 @@ impl<W: Write + io::Seek> ZipWriter<W> { 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) + } + + /// 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)?; + + 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 = file.data_start + file.extra_field.len() as u64; + self.stats.start = header_end; + file.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)?; + } + + self.writing_to_extra_field = false; + self.writing_to_central_extra_field_only = false; + Ok(file.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. @@ -381,6 +618,7 @@ impl<W: Write + io::Seek> ZipWriter<W> { self.start_entry(name, options, Some(raw_values))?; self.writing_to_file = true; + self.writing_raw = true; io::copy(file.get_raw_reader(), self)?; @@ -478,14 +716,51 @@ impl<W: Write + io::Seek> ZipWriter<W> { } let central_size = writer.seek(io::SeekFrom::Current(0))? - central_start; + if self.files.len() > 0xFFFF || central_size > 0xFFFFFFFF || central_start > 0xFFFFFFFF + { + 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 = if self.files.len() > 0xFFFF { + 0xFFFF + } else { + self.files.len() as u16 + }; let footer = spec::CentralDirectoryEnd { disk_number: 0, disk_with_central_directory: 0, - number_of_files_on_this_disk: self.files.len() as u16, - number_of_files: self.files.len() as u16, - central_directory_size: central_size as u32, - central_directory_offset: central_start as u32, - zip_file_comment: self.comment.as_bytes().to_vec(), + zip_file_comment: self.comment.clone(), + number_of_files_on_this_disk: number_of_files, + number_of_files, + central_directory_size: if central_size > 0xFFFFFFFF { + 0xFFFFFFFF + } else { + central_size as u32 + }, + central_directory_offset: if central_start > 0xFFFFFFFF { + 0xFFFFFFFF + } else { + central_start as u32 + }, }; footer.write(writer)?; @@ -553,7 +828,7 @@ impl<W: Write + io::Seek> GenericZipWriter<W> { )), #[cfg(feature = "bzip2")] CompressionMethod::Bzip2 => { - GenericZipWriter::Bzip2(BzEncoder::new(bare, bzip2::Compression::Default)) + GenericZipWriter::Bzip2(BzEncoder::new(bare, bzip2::Compression::default())) } CompressionMethod::Unsupported(..) => { return Err(ZipError::UnsupportedArchive("Unsupported compression")) @@ -637,18 +912,28 @@ fn write_local_file_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipR // crc-32 writer.write_u32::<LittleEndian>(file.crc32)?; // compressed size - writer.write_u32::<LittleEndian>(file.compressed_size as u32)?; + writer.write_u32::<LittleEndian>(if file.compressed_size > 0xFFFFFFFF { + 0xFFFFFFFF + } else { + file.compressed_size as u32 + })?; // uncompressed size - writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?; + writer.write_u32::<LittleEndian>(if file.uncompressed_size > 0xFFFFFFFF { + 0xFFFFFFFF + } else { + 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 = build_extra_field(file)?; - writer.write_u16::<LittleEndian>(extra_field.len() as u16)?; + 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())?; - // extra field - writer.write_all(&extra_field)?; + // zip64 extra field + if file.large_file { + write_local_zip64_extra_field(writer, &file)?; + } Ok(()) } @@ -660,12 +945,37 @@ fn update_local_file_header<T: Write + io::Seek>( const CRC32_OFFSET: u64 = 14; writer.seek(io::SeekFrom::Start(file.header_start + CRC32_OFFSET))?; writer.write_u32::<LittleEndian>(file.crc32)?; - writer.write_u32::<LittleEndian>(file.compressed_size as u32)?; - writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?; + writer.write_u32::<LittleEndian>(if file.compressed_size > 0xFFFFFFFF { + if file.large_file { + 0xFFFFFFFF + } else { + // compressed size can be slightly larger than uncompressed size + return Err(ZipError::Io(io::Error::new( + io::ErrorKind::Other, + "Large file option has not been set", + ))); + } + } else { + file.compressed_size as u32 + })?; + writer.write_u32::<LittleEndian>(if file.uncompressed_size > 0xFFFFFFFF { + // uncompressed size is checked on write to catch it as soon as possible + 0xFFFFFFFF + } else { + file.uncompressed_size as u32 + })?; + if file.large_file { + update_local_zip64_extra_field(writer, file)?; + } 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 @@ -689,14 +999,21 @@ fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData) // crc-32 writer.write_u32::<LittleEndian>(file.crc32)?; // compressed size - writer.write_u32::<LittleEndian>(file.compressed_size as u32)?; + writer.write_u32::<LittleEndian>(if file.compressed_size > 0xFFFFFFFF { + 0xFFFFFFFF + } else { + file.compressed_size as u32 + })?; // uncompressed size - writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?; + writer.write_u32::<LittleEndian>(if file.uncompressed_size > 0xFFFFFFFF { + 0xFFFFFFFF + } else { + 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 = build_extra_field(file)?; - writer.write_u16::<LittleEndian>(extra_field.len() as u16)?; + 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 @@ -706,21 +1023,139 @@ fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData) // external file attributes writer.write_u32::<LittleEndian>(file.external_attributes)?; // relative offset of local header - writer.write_u32::<LittleEndian>(file.header_start as u32)?; + writer.write_u32::<LittleEndian>(if file.header_start > 0xFFFFFFFF { + 0xFFFFFFFF + } else { + file.header_start 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(&extra_field)?; + writer.write_all(&file.extra_field)?; // file comment // <none> Ok(()) } -fn build_extra_field(_file: &ZipFileData) -> ZipResult<Vec<u8>> { - let writer = Vec::new(); - // Future work - Ok(writer) +fn validate_extra_data(file: &ZipFileData) -> ZipResult<()> { + let mut data = file.extra_field.as_slice(); + + if data.len() > 0xFFFF { + return Err(ZipError::Io(io::Error::new( + io::ErrorKind::InvalidData, + "Extra data exceeds extra field", + ))); + } + + while data.len() > 0 { + 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 > 0xFFFFFFFF; + let compressed_size = file.compressed_size > 0xFFFFFFFF; + let header_start = file.header_start > 0xFFFFFFFF; + 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 { @@ -791,6 +1226,7 @@ mod test { compression_method: CompressionMethod::Stored, last_modified_time: DateTime::default(), permissions: Some(33188), + large_file: false, }; writer.start_file("mimetype", options).unwrap(); writer @@ -819,3 +1255,12 @@ mod test { 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, +]; diff --git a/src/zipcrypto.rs b/src/zipcrypto.rs index 32e8af8..3196ea3 100644 --- a/src/zipcrypto.rs +++ b/src/zipcrypto.rs @@ -57,6 +57,11 @@ pub struct ZipCryptoReader<R> { keys: ZipCryptoKeys, } +pub enum ZipCryptoValidator { + PkzipCrc32(u32), + InfoZipMsdosTime(u16), +} + impl<R: std::io::Read> ZipCryptoReader<R> { /// Note: The password is `&[u8]` and not `&str` because the /// [zip specification](https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.3.3.TXT) @@ -81,7 +86,7 @@ impl<R: std::io::Read> ZipCryptoReader<R> { /// Read the ZipCrypto header bytes and validate the password. pub fn validate( mut self, - crc32_plaintext: u32, + validator: ZipCryptoValidator, ) -> Result<Option<ZipCryptoReaderValid<R>>, std::io::Error> { // ZipCrypto prefixes a file with a 12 byte header let mut header_buf = [0u8; 12]; @@ -90,13 +95,30 @@ impl<R: std::io::Read> ZipCryptoReader<R> { *byte = self.keys.decrypt_byte(*byte); } - // PKZIP before 2.0 used 2 byte CRC check. - // PKZIP 2.0+ used 1 byte CRC check. It's more secure. - // We also use 1 byte CRC. - - if (crc32_plaintext >> 24) as u8 != header_buf[11] { - return Ok(None); // Wrong password + match validator { + ZipCryptoValidator::PkzipCrc32(crc32_plaintext) => { + // PKZIP before 2.0 used 2 byte CRC check. + // PKZIP 2.0+ used 1 byte CRC check. It's more secure. + // We also use 1 byte CRC. + + if (crc32_plaintext >> 24) as u8 != header_buf[11] { + return Ok(None); // Wrong password + } + } + ZipCryptoValidator::InfoZipMsdosTime(last_mod_time) => { + // Info-ZIP modification to ZipCrypto format: + // If bit 3 of the general purpose bit flag is set + // (indicates that the file uses a data-descriptor section), + // it uses high byte of 16-bit File Time. + // Info-ZIP code probably writes 2 bytes of File Time. + // We check only 1 byte. + + if (last_mod_time >> 8) as u8 != header_buf[11] { + return Ok(None); // Wrong password + } + } } + Ok(Some(ZipCryptoReaderValid { reader: self })) } } diff --git a/tests/end_to_end.rs b/tests/end_to_end.rs index b826f54..baebd28 100644 --- a/tests/end_to_end.rs +++ b/tests/end_to_end.rs @@ -1,3 +1,4 @@ +use byteorder::{LittleEndian, WriteBytesExt}; use std::collections::HashSet; use std::io::prelude::*; use std::io::{Cursor, Seek}; @@ -46,6 +47,25 @@ fn copy() { check_zip_file_contents(&mut tgt_archive, COPY_ENTRY_NAME); } +// This test asserts that after appending to a `ZipWriter`, then reading its contents back out, +// both the prior data and the appended data will be exactly the same as their originals. +#[test] +fn append() { + let mut file = &mut Cursor::new(Vec::new()); + write_to_zip(file).expect("file written"); + + { + let mut zip = zip::ZipWriter::new_append(&mut file).unwrap(); + zip.start_file(COPY_ENTRY_NAME, Default::default()).unwrap(); + zip.write_all(LOREM_IPSUM).unwrap(); + zip.finish().unwrap(); + } + + let mut zip = zip::ZipArchive::new(&mut file).unwrap(); + check_zip_file_contents(&mut zip, ENTRY_NAME); + check_zip_file_contents(&mut zip, COPY_ENTRY_NAME); +} + fn write_to_zip(file: &mut Cursor<Vec<u8>>) -> zip::result::ZipResult<()> { let mut zip = zip::ZipWriter::new(file); @@ -57,6 +77,13 @@ fn write_to_zip(file: &mut Cursor<Vec<u8>>) -> zip::result::ZipResult<()> { zip.start_file("test/☃.txt", options)?; zip.write_all(b"Hello, World!\n")?; + zip.start_file_with_extra_data("test_with_extra_data/🐢.txt", Default::default())?; + 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"Hello, World! Again.\n")?; + zip.start_file(ENTRY_NAME, Default::default())?; zip.write_all(LOREM_IPSUM)?; @@ -65,13 +92,27 @@ fn write_to_zip(file: &mut Cursor<Vec<u8>>) -> zip::result::ZipResult<()> { } fn read_zip<R: Read + Seek>(zip_file: R) -> zip::result::ZipResult<zip::ZipArchive<R>> { - let archive = zip::ZipArchive::new(zip_file).unwrap(); - - let expected_file_names = ["test/", "test/☃.txt", ENTRY_NAME]; + let mut archive = zip::ZipArchive::new(zip_file).unwrap(); + + let expected_file_names = [ + "test/", + "test/☃.txt", + "test_with_extra_data/🐢.txt", + ENTRY_NAME, + ]; let expected_file_names = HashSet::from_iter(expected_file_names.iter().map(|&v| v)); let file_names = archive.file_names().collect::<HashSet<_>>(); assert_eq!(file_names, expected_file_names); + { + let file_with_extra_data = archive.by_name("test_with_extra_data/🐢.txt")?; + let mut extra_data = Vec::new(); + extra_data.write_u16::<LittleEndian>(0xbeef)?; + extra_data.write_u16::<LittleEndian>(EXTRA_DATA.len() as u16)?; + extra_data.write_all(EXTRA_DATA)?; + assert_eq!(file_with_extra_data.extra_data(), extra_data.as_slice()); + } + Ok(archive) } @@ -103,6 +144,8 @@ vitae tristique consectetur, neque lectus pulvinar dui, sed feugiat purus diam i inceptos himenaeos. Maecenas feugiat velit in ex ultrices scelerisque id id neque. "; +const EXTRA_DATA: &'static [u8] = b"Extra Data"; + const ENTRY_NAME: &str = "test/lorem_ipsum.txt"; const COPY_ENTRY_NAME: &str = "test/lorem_ipsum_renamed.txt"; diff --git a/tests/zip_crypto.rs b/tests/zip_crypto.rs index cae6b1f..6c4d6b8 100644 --- a/tests/zip_crypto.rs +++ b/tests/zip_crypto.rs @@ -47,9 +47,9 @@ fn encrypted_file() { // No password let file = archive.by_index(0); match file { - Err(zip::result::ZipError::UnsupportedArchive("Password required to decrypt file")) => { - () - } + Err(zip::result::ZipError::UnsupportedArchive( + zip::result::ZipError::PASSWORD_REQUIRED, + )) => (), Err(_) => panic!( "Expected PasswordRequired error when opening encrypted file without password" ), |