diff --git a/src/read.rs b/src/read.rs index d76a780f..c619f24c 100644 --- a/src/read.rs +++ b/src/read.rs @@ -640,43 +640,70 @@ pub(crate) fn central_header_to_zip_file( archive_offset: u64, ) -> ZipResult { let central_header_start = reader.seek(io::SeekFrom::Current(0))?; - let central_header = spec::CentralDirectoryHeader::parse(reader)?; + // Parse central header + let signature = reader.read_u32::()?; + if signature != spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE { + return Err(ZipError::InvalidArchive("Invalid Central Directory header")); + } - let file_name = match central_header.flags.is_utf8() { - true => String::from_utf8_lossy(&*central_header.file_name_raw).into_owned(), - false => central_header.file_name_raw.clone().from_cp437(), + let version_made_by = reader.read_u16::()?; + let _version_to_extract = reader.read_u16::()?; + let flags = reader.read_u16::()?; + 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::()?; + let last_mod_time = reader.read_u16::()?; + let last_mod_date = reader.read_u16::()?; + let crc32 = reader.read_u32::()?; + let compressed_size = reader.read_u32::()?; + let uncompressed_size = reader.read_u32::()?; + let file_name_length = reader.read_u16::()? as usize; + let extra_field_length = reader.read_u16::()? as usize; + let file_comment_length = reader.read_u16::()? as usize; + let _disk_number = reader.read_u16::()?; + let _internal_file_attributes = reader.read_u16::()?; + let external_file_attributes = reader.read_u32::()?; + let offset = reader.read_u32::()? as u64; + let mut file_name_raw = vec![0; file_name_length]; + reader.read_exact(&mut file_name_raw)?; + let mut extra_field = vec![0; extra_field_length]; + reader.read_exact(&mut extra_field)?; + let mut file_comment_raw = vec![0; file_comment_length]; + reader.read_exact(&mut file_comment_raw)?; + + let file_name = match is_utf8 { + true => String::from_utf8_lossy(&*file_name_raw).into_owned(), + false => file_name_raw.clone().from_cp437(), }; - let file_comment = match central_header.flags.is_utf8() { - true => String::from_utf8_lossy(&*central_header.file_comment_raw).into_owned(), - false => central_header.file_comment_raw.clone().from_cp437(), + let file_comment = match is_utf8 { + true => String::from_utf8_lossy(&*file_comment_raw).into_owned(), + false => file_comment_raw.from_cp437(), }; // Construct the result let mut result = ZipFileData { - system: System::from_u8((central_header.version_made_by >> 8) as u8), - version_made_by: central_header.version_made_by as u8, - encrypted: central_header.flags.encrypted(), - using_data_descriptor: central_header.flags.using_data_descriptor(), + 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(central_header.compression_method) + CompressionMethod::from_u16(compression_method) }, compression_level: None, - last_modified_time: DateTime::from_msdos( - central_header.last_mod_date, - central_header.last_mod_time, - ), - crc32: central_header.crc32, - compressed_size: central_header.compressed_size as u64, - uncompressed_size: central_header.uncompressed_size as u64, + last_modified_time: DateTime::from_msdos(last_mod_date, last_mod_time), + crc32, + compressed_size: compressed_size as u64, + uncompressed_size: uncompressed_size as u64, file_name, - file_name_raw: central_header.file_name_raw, - extra_field: central_header.extra_field, + file_name_raw, + extra_field, file_comment, - header_start: central_header.offset as u64, + header_start: offset, central_header_start, data_start: AtomicU64::new(0), - external_attributes: central_header.external_file_attributes, + external_attributes: external_file_attributes, large_file: false, aes_mode: None, }; diff --git a/src/spec.rs b/src/spec.rs index 1f826b93..3ffcf732 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -1,4 +1,3 @@ -#![allow(missing_docs, dead_code)] use crate::result::{ZipError, ZipResult}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use std::io; @@ -9,12 +8,10 @@ pub const CENTRAL_DIRECTORY_HEADER_SIGNATURE: u32 = 0x02014b50; const CENTRAL_DIRECTORY_END_SIGNATURE: u32 = 0x06054b50; pub const ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE: u32 = 0x06064b50; const ZIP64_CENTRAL_DIRECTORY_END_LOCATOR_SIGNATURE: u32 = 0x07064b50; -pub const DATA_DESCRIPTOR_SIGNATURE: u32 = 0x08074b50; pub const ZIP64_BYTES_THR: u64 = u32::MAX as u64; pub const ZIP64_ENTRY_THR: usize = u16::MAX as usize; -#[derive(Clone, Debug, PartialEq)] pub struct CentralDirectoryEnd { pub disk_number: u16, pub disk_with_central_directory: u16, @@ -26,10 +23,6 @@ pub struct CentralDirectoryEnd { } impl CentralDirectoryEnd { - pub fn len(&self) -> usize { - 22 + self.zip_file_comment.len() - } - pub fn parse(reader: &mut T) -> ZipResult { let magic = reader.read_u32::()?; if magic != CENTRAL_DIRECTORY_END_SIGNATURE { @@ -212,315 +205,3 @@ impl Zip64CentralDirectoryEnd { Ok(()) } } - -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct GeneralPurposeBitFlags(pub u16); - -impl GeneralPurposeBitFlags { - #[inline] - pub fn encrypted(&self) -> bool { - self.0 & 1 == 1 - } - - #[inline] - pub fn is_utf8(&self) -> bool { - self.0 & (1 << 11) != 0 - } - - #[inline] - pub fn using_data_descriptor(&self) -> bool { - self.0 & (1 << 3) != 0 - } - - #[inline] - pub fn set_using_data_descriptor(&mut self, b: bool) { - self.0 &= !(1 << 3); - if b { - self.0 |= 1 << 3; - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct CentralDirectoryHeader { - pub version_made_by: u16, - pub version_to_extract: u16, - pub flags: GeneralPurposeBitFlags, - pub compression_method: u16, - pub last_mod_time: u16, - pub last_mod_date: u16, - pub crc32: u32, - pub compressed_size: u32, - pub uncompressed_size: u32, - pub disk_number: u16, - pub internal_file_attributes: u16, - pub external_file_attributes: u32, - pub offset: u32, - pub file_name_raw: Vec, - pub extra_field: Vec, - pub file_comment_raw: Vec, -} - -impl CentralDirectoryHeader { - pub fn len(&self) -> usize { - 46 + self.file_name_raw.len() + self.extra_field.len() + self.file_comment_raw.len() - } - pub fn parse(reader: &mut R) -> ZipResult { - let signature = reader.read_u32::()?; - if signature != CENTRAL_DIRECTORY_HEADER_SIGNATURE { - return Err(ZipError::InvalidArchive("Invalid Central Directory header")); - } - - let version_made_by = reader.read_u16::()?; - let version_to_extract = reader.read_u16::()?; - let flags = reader.read_u16::()?; - let compression_method = reader.read_u16::()?; - let last_mod_time = reader.read_u16::()?; - let last_mod_date = reader.read_u16::()?; - let crc32 = reader.read_u32::()?; - let compressed_size = reader.read_u32::()?; - let uncompressed_size = reader.read_u32::()?; - let file_name_length = reader.read_u16::()?; - let extra_field_length = reader.read_u16::()?; - let file_comment_length = reader.read_u16::()?; - let disk_number = reader.read_u16::()?; - let internal_file_attributes = reader.read_u16::()?; - let external_file_attributes = reader.read_u32::()?; - let offset = reader.read_u32::()?; - let mut file_name_raw = vec![0; file_name_length as usize]; - reader.read_exact(&mut file_name_raw)?; - let mut extra_field = vec![0; extra_field_length as usize]; - reader.read_exact(&mut extra_field)?; - let mut file_comment_raw = vec![0; file_comment_length as usize]; - reader.read_exact(&mut file_comment_raw)?; - - Ok(CentralDirectoryHeader { - version_made_by, - version_to_extract, - flags: GeneralPurposeBitFlags(flags), - compression_method, - last_mod_time, - last_mod_date, - crc32, - compressed_size, - uncompressed_size, - disk_number, - internal_file_attributes, - external_file_attributes, - offset, - file_name_raw, - extra_field, - file_comment_raw, - }) - } - - pub fn write(&self, writer: &mut T) -> ZipResult<()> { - writer.write_u32::(CENTRAL_DIRECTORY_HEADER_SIGNATURE)?; - writer.write_u16::(self.version_made_by)?; - writer.write_u16::(self.version_to_extract)?; - writer.write_u16::(self.flags.0)?; - writer.write_u16::(self.compression_method)?; - writer.write_u16::(self.last_mod_time)?; - writer.write_u16::(self.last_mod_date)?; - writer.write_u32::(self.crc32)?; - writer.write_u32::(self.compressed_size)?; - writer.write_u32::(self.uncompressed_size)?; - writer.write_u16::(self.file_name_raw.len() as u16)?; - writer.write_u16::(self.extra_field.len() as u16)?; - writer.write_u16::(self.file_comment_raw.len() as u16)?; - writer.write_u16::(self.disk_number)?; - writer.write_u16::(self.internal_file_attributes)?; - writer.write_u32::(self.external_file_attributes)?; - writer.write_u32::(self.offset)?; - writer.write_all(&self.file_name_raw)?; - writer.write_all(&self.extra_field)?; - writer.write_all(&self.file_comment_raw)?; - Ok(()) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct LocalFileHeader { - pub version_to_extract: u16, - pub flags: GeneralPurposeBitFlags, - pub compression_method: u16, - pub last_mod_time: u16, - pub last_mod_date: u16, - pub crc32: u32, - pub compressed_size: u32, - pub uncompressed_size: u32, - pub file_name_raw: Vec, - pub extra_field: Vec, -} - -impl LocalFileHeader { - pub fn len(&self) -> usize { - 30 + self.file_name_raw.len() + self.extra_field.len() - } - - pub fn parse(reader: &mut R) -> ZipResult { - let signature = reader.read_u32::()?; - if signature != LOCAL_FILE_HEADER_SIGNATURE { - return Err(ZipError::InvalidArchive("Invalid local file header")); - } - - let version_to_extract = reader.read_u16::()?; - let flags = reader.read_u16::()?; - let compression_method = reader.read_u16::()?; - let last_mod_time = reader.read_u16::()?; - let last_mod_date = reader.read_u16::()?; - let crc32 = reader.read_u32::()?; - let compressed_size = reader.read_u32::()?; - let uncompressed_size = reader.read_u32::()?; - let file_name_length = reader.read_u16::()?; - let extra_field_length = reader.read_u16::()?; - - let mut file_name_raw = vec![0; file_name_length as usize]; - reader.read_exact(&mut file_name_raw)?; - let mut extra_field = vec![0; extra_field_length as usize]; - reader.read_exact(&mut extra_field)?; - - Ok(LocalFileHeader { - version_to_extract, - flags: GeneralPurposeBitFlags(flags), - compression_method, - last_mod_time, - last_mod_date, - crc32, - compressed_size, - uncompressed_size, - file_name_raw, - extra_field, - }) - } - - pub fn write(&self, writer: &mut T) -> ZipResult<()> { - writer.write_u32::(LOCAL_FILE_HEADER_SIGNATURE)?; - writer.write_u16::(self.version_to_extract)?; - writer.write_u16::(self.flags.0)?; - writer.write_u16::(self.compression_method)?; - writer.write_u16::(self.last_mod_time)?; - writer.write_u16::(self.last_mod_date)?; - writer.write_u32::(self.crc32)?; - writer.write_u32::(self.compressed_size)?; - writer.write_u32::(self.uncompressed_size)?; - writer.write_u16::(self.file_name_raw.len() as u16)?; - writer.write_u16::(self.extra_field.len() as u16)?; - writer.write_all(&self.file_name_raw)?; - writer.write_all(&self.extra_field)?; - Ok(()) - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct DataDescriptor { - pub crc32: u32, - pub compressed_size: u32, - pub uncompressed_size: u32, -} - -impl DataDescriptor { - pub fn read(reader: &mut T) -> ZipResult { - let first_word = reader.read_u32::()?; - let crc32 = if first_word == DATA_DESCRIPTOR_SIGNATURE { - reader.read_u32::()? - } else { - first_word - }; - let compressed_size = reader.read_u32::()?; - let uncompressed_size = reader.read_u32::()?; - Ok(DataDescriptor { - crc32, - compressed_size, - uncompressed_size, - }) - } - - pub fn write(&self, writer: &mut T) -> ZipResult<()> { - writer.write_u32::(DATA_DESCRIPTOR_SIGNATURE)?; - writer.write_u32::(self.crc32)?; - writer.write_u32::(self.compressed_size)?; - writer.write_u32::(self.uncompressed_size)?; - Ok(()) - } -} - -#[cfg(test)] -mod test { - use super::{ - CentralDirectoryHeader, DataDescriptor, GeneralPurposeBitFlags, LocalFileHeader, ZipResult, - }; - use std::io::Cursor; - #[test] - fn test_cdh_roundtrip() -> ZipResult<()> { - let cdh1 = CentralDirectoryHeader { - version_made_by: 1, - version_to_extract: 2, - flags: GeneralPurposeBitFlags(3), - compression_method: 4, - last_mod_time: 5, - last_mod_date: 6, - crc32: 7, - compressed_size: 8, - uncompressed_size: 9, - disk_number: 10, - internal_file_attributes: 11, - external_file_attributes: 12, - offset: 13, - file_name_raw: b"a".to_vec(), - extra_field: b"bb".to_vec(), - file_comment_raw: b"ccc".to_vec(), - }; - let mut bytes = Vec::new(); - { - let mut cursor = Cursor::new(&mut bytes); - cdh1.write(&mut cursor)?; - } - let cdh2 = CentralDirectoryHeader::parse(&mut &bytes[..])?; - assert_eq!(cdh1, cdh2); - Ok(()) - } - - #[test] - fn test_lfh_roundtrip() -> ZipResult<()> { - let lfh1 = LocalFileHeader { - version_to_extract: 1, - flags: GeneralPurposeBitFlags(2), - compression_method: 3, - last_mod_time: 4, - last_mod_date: 5, - crc32: 6, - compressed_size: 7, - uncompressed_size: 8, - file_name_raw: b"a".to_vec(), - extra_field: b"bb".to_vec(), - }; - let mut bytes = Vec::new(); - { - let mut cursor = Cursor::new(&mut bytes); - lfh1.write(&mut cursor)?; - } - let lfh2 = LocalFileHeader::parse(&mut &bytes[..])?; - assert_eq!(lfh1, lfh2); - Ok(()) - } - - #[test] - fn test_dd_roundtrip() -> ZipResult<()> { - let dd1 = DataDescriptor { - crc32: 1, - compressed_size: 2, - uncompressed_size: 3, - }; - let mut bytes = Vec::new(); - { - let mut cursor = Cursor::new(&mut bytes); - dd1.write(&mut cursor)?; - } - let dd2 = DataDescriptor::read(&mut &bytes[..])?; - assert_eq!(dd1, dd2); - let dd3 = DataDescriptor::read(&mut &bytes[4..])?; - assert_eq!(dd1, dd3); - Ok(()) - } -} diff --git a/src/write.rs b/src/write.rs index 2e398b91..3d0e0170 100644 --- a/src/write.rs +++ b/src/write.rs @@ -998,38 +998,44 @@ fn clamp_opt(value: T, range: RangeInclusive) -> Option { } fn write_local_file_header(writer: &mut T, file: &ZipFileData) -> ZipResult<()> { - let flags = if !file.file_name.is_ascii() { + // local file header signature + writer.write_u32::(spec::LOCAL_FILE_HEADER_SIGNATURE)?; + // version needed to extract + writer.write_u16::(file.version_needed())?; + // general purpose bit flag + let flag = if !file.file_name.is_ascii() { 1u16 << 11 } else { 0 }; + writer.write_u16::(flag)?; + // Compression method #[allow(deprecated)] - let compression_method = file.compression_method.to_u16(); - let compressed_size = file.compressed_size.min(spec::ZIP64_BYTES_THR) as u32; - let uncompressed_size = file.uncompressed_size.min(spec::ZIP64_BYTES_THR) as u32; - - let mut extra_field = if file.large_file { - let mut zip64_extra_field = vec![0; 20]; - write_local_zip64_extra_field(&mut zip64_extra_field, file)?; - zip64_extra_field + writer.write_u16::(file.compression_method.to_u16())?; + // last mod file time and last mod file date + writer.write_u16::(file.last_modified_time.timepart())?; + writer.write_u16::(file.last_modified_time.datepart())?; + // crc-32 + writer.write_u32::(file.crc32)?; + // compressed size and uncompressed size + if file.large_file { + writer.write_u32::(spec::ZIP64_BYTES_THR as u32)?; + writer.write_u32::(spec::ZIP64_BYTES_THR as u32)?; } else { - Vec::new() - }; - extra_field.extend_from_slice(&file.extra_field[..]); - - let local_file_header = spec::LocalFileHeader { - version_to_extract: file.version_needed(), - flags: spec::GeneralPurposeBitFlags(flags), - compression_method, - last_mod_time: file.last_modified_time.timepart(), - last_mod_date: file.last_modified_time.datepart(), - crc32: file.crc32, - compressed_size, - uncompressed_size, - file_name_raw: file.file_name.as_bytes().to_vec(), - extra_field, - }; - local_file_header.write(writer)?; + writer.write_u32::(file.compressed_size as u32)?; + writer.write_u32::(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_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())?; + // zip64 extra field + if file.large_file { + write_local_zip64_extra_field(writer, file)?; + } Ok(()) } @@ -1064,40 +1070,56 @@ fn write_central_directory_header(writer: &mut T, file: &ZipFileData) let zip64_extra_field_length = write_central_zip64_extra_field(&mut zip64_extra_field.as_mut(), file)?; - let flags = if !file.file_name.is_ascii() { + // central file header signature + writer.write_u32::(spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE)?; + // version made by + let version_made_by = (file.system as u16) << 8 | (file.version_made_by as u16); + writer.write_u16::(version_made_by)?; + // version needed to extract + writer.write_u16::(file.version_needed())?; + // general puprose bit flag + let flag = if !file.file_name.is_ascii() { 1u16 << 11 } else { 0 }; + writer.write_u16::(flag)?; + // compression method #[allow(deprecated)] - let compression_method = file.compression_method.to_u16(); - let compressed_size = file.compressed_size.min(spec::ZIP64_BYTES_THR) as u32; - let uncompressed_size = file.uncompressed_size.min(spec::ZIP64_BYTES_THR) as u32; - let offset = file.header_start.min(spec::ZIP64_BYTES_THR) as u32; + writer.write_u16::(file.compression_method.to_u16())?; + // last mod file time + date + writer.write_u16::(file.last_modified_time.timepart())?; + writer.write_u16::(file.last_modified_time.datepart())?; + // crc-32 + writer.write_u32::(file.crc32)?; + // compressed size + writer.write_u32::(file.compressed_size.min(spec::ZIP64_BYTES_THR) as u32)?; + // uncompressed size + writer.write_u32::(file.uncompressed_size.min(spec::ZIP64_BYTES_THR) as u32)?; + // file name length + writer.write_u16::(file.file_name.as_bytes().len() as u16)?; + // extra field length + writer.write_u16::(zip64_extra_field_length + file.extra_field.len() as u16)?; + // file comment length + writer.write_u16::(0)?; + // disk number start + writer.write_u16::(0)?; + // internal file attribytes + writer.write_u16::(0)?; + // external file attributes + writer.write_u32::(file.external_attributes)?; + // relative offset of local header + writer.write_u32::(file.header_start.min(spec::ZIP64_BYTES_THR) as u32)?; + // file name + writer.write_all(file.file_name.as_bytes())?; + // zip64 extra field + writer.write_all(&zip64_extra_field[..zip64_extra_field_length as usize])?; + // extra field + writer.write_all(&file.extra_field)?; + // file comment + // - let mut extra_field = zip64_extra_field[..zip64_extra_field_length as usize].to_vec(); - extra_field.extend_from_slice(&file.extra_field[..]); - - let header = spec::CentralDirectoryHeader { - version_made_by: (file.system as u16) << 8 | (file.version_made_by as u16), - version_to_extract: file.version_needed(), - flags: spec::GeneralPurposeBitFlags(flags), - compression_method, - last_mod_time: file.last_modified_time.timepart(), - last_mod_date: file.last_modified_time.datepart(), - crc32: file.crc32, - compressed_size, - uncompressed_size, - disk_number: 0, - internal_file_attributes: 0, - external_file_attributes: file.external_attributes, - offset, - file_name_raw: file.file_name.as_bytes().to_vec(), - extra_field, - file_comment_raw: Vec::new(), - }; - - header.write(writer) + Ok(()) } fn validate_extra_data(file: &ZipFileData) -> ZipResult<()> {