diff --git a/Cargo.toml b/Cargo.toml index 11a15a12..7bb5a260 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ walkdir = "2" deflate = ["flate2/rust_backend"] deflate-miniz = ["flate2/default"] deflate-zlib = ["flate2/zlib"] +unreserved = [] default = ["bzip2", "deflate", "time"] [[bench]] diff --git a/src/read.rs b/src/read.rs index 8d8c4b17..a79b0099 100644 --- a/src/read.rs +++ b/src/read.rs @@ -592,14 +592,16 @@ pub(crate) fn central_header_to_zip_file( 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), } @@ -610,20 +612,22 @@ pub(crate) fn central_header_to_zip_file( 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::()?; let len = reader.read_u16::()?; 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::()?; len_left -= 8; } if file.compressed_size == 0xFFFFFFFF { + file.large_file = true; file.compressed_size = reader.read_u64::()?; len_left -= 8; } @@ -815,6 +819,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 @@ -933,6 +942,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. @@ -943,9 +953,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), } diff --git a/src/spec.rs b/src/spec.rs index 91966b67..4ab3656d 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -117,6 +117,14 @@ impl Zip64CentralDirectoryEndLocator { number_of_disks, }) } + + pub fn write(&self, writer: &mut T) -> ZipResult<()> { + writer.write_u32::(ZIP64_CENTRAL_DIRECTORY_END_LOCATOR_SIGNATURE)?; + writer.write_u32::(self.disk_with_central_directory)?; + writer.write_u64::(self.end_of_central_directory_offset)?; + writer.write_u32::(self.number_of_disks)?; + Ok(()) + } } pub struct Zip64CentralDirectoryEnd { @@ -179,4 +187,18 @@ impl Zip64CentralDirectoryEnd { "Could not find ZIP64 central directory end", )) } + + pub fn write(&self, writer: &mut T) -> ZipResult<()> { + writer.write_u32::(ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE)?; + writer.write_u64::(44)?; // record size + writer.write_u16::(self.version_made_by)?; + writer.write_u16::(self.version_needed_to_extract)?; + writer.write_u32::(self.disk_number)?; + writer.write_u32::(self.disk_with_central_directory)?; + writer.write_u64::(self.number_of_files_on_this_disk)?; + writer.write_u64::(self.number_of_files)?; + writer.write_u64::(self.central_directory_size)?; + writer.write_u64::(self.central_directory_offset)?; + Ok(()) + } } diff --git a/src/types.rs b/src/types.rs index 06cee7cd..026aa150 100644 --- a/src/types.rs +++ b/src/types.rs @@ -232,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, + /// Extra field usually used for storage expansion + pub extra_field: Vec, /// File comment pub file_comment: String, /// Specifies where the local header of the file starts @@ -244,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 { @@ -277,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, } } @@ -313,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 f14cf827..05c3666a 100644 --- a/src/write.rs +++ b/src/write.rs @@ -5,7 +5,7 @@ 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 { files: Vec, stats: ZipWriterStats, writing_to_file: bool, - comment: Vec, + writing_to_extra_field: bool, + writing_to_central_extra_field_only: bool, writing_raw: bool, + comment: Vec, } #[derive(Default)] @@ -91,6 +93,7 @@ pub struct FileOptions { compression_method: CompressionMethod, last_modified_time: DateTime, permissions: Option, + 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 Write for ZipWriter { } 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, @@ -225,6 +251,8 @@ impl ZipWriter { 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 }) @@ -241,8 +269,10 @@ impl ZipWriter { files: Vec::new(), stats: Default::default(), writing_to_file: false, - comment: Vec::new(), + writing_to_extra_field: false, + writing_to_central_extra_field_only: false, writing_raw: false, + comment: Vec::new(), } } @@ -274,7 +304,6 @@ impl ZipWriter { { self.finish_file()?; - let is_raw = raw_values.is_some(); let raw_values = raw_values.unwrap_or_else(|| ZipRawValues { crc32: 0, compressed_size: 0, @@ -298,11 +327,13 @@ impl ZipWriter { 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)?; @@ -316,17 +347,14 @@ impl ZipWriter { 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(); @@ -362,13 +390,14 @@ impl ZipWriter { } *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", @@ -382,6 +411,168 @@ impl ZipWriter { 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( + &mut self, + name: S, + options: FileOptions, + align: u16, + ) -> Result + where + S: Into, + { + 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::(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::(0xbeef)?; + /// zip.write_u16::(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::(0xbeef)?; + /// zip.write_u16::(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::(0xdead)?; + /// zip.write_u16::(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::(0xbeef)?; + /// zip.write_u16::(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( + &mut self, + name: S, + mut options: FileOptions, + ) -> ZipResult + where + S: Into, + { + 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 { + 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 { + // 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::(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. @@ -427,6 +618,7 @@ impl ZipWriter { self.start_entry(name, options, Some(raw_values))?; self.writing_to_file = true; + self.writing_raw = true; io::copy(file.get_raw_reader(), self)?; @@ -524,14 +716,51 @@ impl ZipWriter { } 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.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)?; @@ -683,18 +912,28 @@ fn write_local_file_header(writer: &mut T, file: &ZipFileData) -> ZipR // crc-32 writer.write_u32::(file.crc32)?; // compressed size - writer.write_u32::(file.compressed_size as u32)?; + writer.write_u32::(if file.compressed_size > 0xFFFFFFFF { + 0xFFFFFFFF + } else { + file.compressed_size as u32 + })?; // uncompressed size - writer.write_u32::(file.uncompressed_size as u32)?; + writer.write_u32::(if file.uncompressed_size > 0xFFFFFFFF { + 0xFFFFFFFF + } else { + file.uncompressed_size as u32 + })?; // file name length writer.write_u16::(file.file_name.as_bytes().len() as u16)?; // extra field length - let extra_field = build_extra_field(file)?; - writer.write_u16::(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::(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(()) } @@ -706,12 +945,37 @@ fn update_local_file_header( const CRC32_OFFSET: u64 = 14; writer.seek(io::SeekFrom::Start(file.header_start + CRC32_OFFSET))?; writer.write_u32::(file.crc32)?; - writer.write_u32::(file.compressed_size as u32)?; - writer.write_u32::(file.uncompressed_size as u32)?; + writer.write_u32::(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::(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(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::(spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE)?; // version made by @@ -735,14 +999,21 @@ fn write_central_directory_header(writer: &mut T, file: &ZipFileData) // crc-32 writer.write_u32::(file.crc32)?; // compressed size - writer.write_u32::(file.compressed_size as u32)?; + writer.write_u32::(if file.compressed_size > 0xFFFFFFFF { + 0xFFFFFFFF + } else { + file.compressed_size as u32 + })?; // uncompressed size - writer.write_u32::(file.uncompressed_size as u32)?; + writer.write_u32::(if file.uncompressed_size > 0xFFFFFFFF { + 0xFFFFFFFF + } else { + file.uncompressed_size as u32 + })?; // file name length writer.write_u16::(file.file_name.as_bytes().len() as u16)?; // extra field length - let extra_field = build_extra_field(file)?; - writer.write_u16::(extra_field.len() as u16)?; + writer.write_u16::(zip64_extra_field_length + file.extra_field.len() as u16)?; // file comment length writer.write_u16::(0)?; // disk number start @@ -752,21 +1023,139 @@ fn write_central_directory_header(writer: &mut T, file: &ZipFileData) // external file attributes writer.write_u32::(file.external_attributes)?; // relative offset of local header - writer.write_u32::(file.header_start as u32)?; + writer.write_u32::(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 // Ok(()) } -fn build_extra_field(_file: &ZipFileData) -> ZipResult> { - 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::()?; + let size = data.read_u16::()? 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(writer: &mut T, file: &ZipFileData) -> ZipResult<()> { + // This entry in the Local header MUST include BOTH original + // and compressed file size fields. + writer.write_u16::(0x0001)?; + writer.write_u16::(16)?; + writer.write_u64::(file.uncompressed_size)?; + writer.write_u64::(file.compressed_size)?; + // Excluded fields: + // u32: disk start number + Ok(()) +} + +fn update_local_zip64_extra_field( + 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::(file.uncompressed_size)?; + writer.write_u64::(file.compressed_size)?; + // Excluded fields: + // u32: disk start number + Ok(()) +} + +fn write_central_zip64_extra_field(writer: &mut T, file: &ZipFileData) -> ZipResult { + // 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::(0x0001)?; + writer.write_u16::(size)?; + size += 4; + + if uncompressed_size { + writer.write_u64::(file.uncompressed_size)?; + } + if compressed_size { + writer.write_u64::(file.compressed_size)?; + } + if header_start { + writer.write_u64::(file.header_start)?; + } + // Excluded fields: + // u32: disk start number + } + Ok(size) } fn path_to_string(path: &std::path::Path) -> String { @@ -837,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 @@ -865,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/tests/end_to_end.rs b/tests/end_to_end.rs index c658a6f5..baebd287 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}; @@ -76,6 +77,13 @@ fn write_to_zip(file: &mut Cursor>) -> 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::(0xbeef)?; + zip.write_u16::(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)?; @@ -84,13 +92,27 @@ fn write_to_zip(file: &mut Cursor>) -> zip::result::ZipResult<()> { } fn read_zip(zip_file: R) -> zip::result::ZipResult> { - let archive = zip::ZipArchive::new(zip_file).unwrap(); + let mut archive = zip::ZipArchive::new(zip_file).unwrap(); - let expected_file_names = ["test/", "test/☃.txt", ENTRY_NAME]; + 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::>(); 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::(0xbeef)?; + extra_data.write_u16::(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) } @@ -122,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";