diff --git a/src/read.rs b/src/read.rs index 053292ff7..08277383a 100644 --- a/src/read.rs +++ b/src/read.rs @@ -870,7 +870,7 @@ pub(crate) fn central_header_to_zip_file( archive_offset: u64, ) -> ZipResult { let central_header_start = reader.stream_position()?; - + let central_header = spec::CentralDirectoryHeader::parse(reader)?; // Parse central header let signature = reader.read_u32_le()?; if signature != spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE { @@ -881,11 +881,12 @@ pub(crate) fn central_header_to_zip_file( } /// Parse a central directory entry to collect the information for the file. -fn central_header_to_zip_file_inner( +fn central_header_to_zip_file_inner( reader: &mut R, archive_offset: u64, central_header_start: u64, ) -> ZipResult { + let central_header = spec::CentralDirectoryHeader::parse(reader)?; let version_made_by = reader.read_u16_le()?; let _version_to_extract = reader.read_u16_le()?; let flags = reader.read_u16_le()?; @@ -923,29 +924,31 @@ fn central_header_to_zip_file_inner( // Construct the result let mut result = ZipFileData { - system: System::from((version_made_by >> 8) as u8), - version_made_by: version_made_by as u8, - encrypted, - using_data_descriptor, + 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(), compression_method: { #[allow(deprecated)] - CompressionMethod::from_u16(compression_method) + CompressionMethod::from_u16(central_header.compression_method) }, compression_level: None, - 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, + 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, file_name, - file_name_raw: file_name_raw.into(), - extra_field: Some(Arc::new(extra_field)), - central_extra_field: None, + file_name_raw: central_header.file_name_raw, + extra_field: Some(Arc::new(central_header.extra_field)), file_comment, - header_start: offset, + header_start: central_header.offset as u64, extra_data_start: None, central_header_start, data_start: OnceLock::new(), - external_attributes: external_file_attributes, + external_attributes: central_header.external_file_attributes, large_file: false, aes_mode: None, aes_extra_data_start: 0, diff --git a/src/read/stream.rs b/src/read/stream.rs index 40cb9efc8..a8a0a90cf 100644 --- a/src/read/stream.rs +++ b/src/read/stream.rs @@ -1,6 +1,6 @@ use crate::unstable::LittleEndianReadExt; use std::fs; -use std::io::{self, Read}; +use std::io::{self, Read, Seek}; use std::path::{Path, PathBuf}; use super::{ @@ -19,7 +19,7 @@ impl ZipStreamReader { } } -impl ZipStreamReader { +impl ZipStreamReader { fn parse_central_directory(&mut self) -> ZipResult> { // Give archive_offset and central_header_start dummy value 0, since // they are not used in the output. diff --git a/src/spec.rs b/src/spec.rs index 89481faf9..b6aeba6ef 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs, dead_code)] use crate::result::{ZipError, ZipResult}; use crate::unstable::{LittleEndianReadExt, LittleEndianWriteExt}; use core::mem::size_of_val; @@ -11,10 +12,12 @@ pub const CENTRAL_DIRECTORY_HEADER_SIGNATURE: u32 = 0x02014b50; pub(crate) const CENTRAL_DIRECTORY_END_SIGNATURE: u32 = 0x06054b50; pub const ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE: u32 = 0x06064b50; pub(crate) 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,6 +29,10 @@ 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_le()?; if magic != CENTRAL_DIRECTORY_END_SIGNATURE { @@ -273,3 +280,315 @@ pub(crate) fn path_to_string>(path: T) -> Box { maybe_original.unwrap().into() } } + +#[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/types.rs b/src/types.rs index 00a851da1..924eb34da 100644 --- a/src/types.rs +++ b/src/types.rs @@ -429,7 +429,6 @@ impl ZipFileData { _ => None, } } - /// PKZIP version needed to open this file (from APPNOTE 4.4.3.2). pub fn version_needed(&self) -> u16 { let compression_version: u16 = match self.compression_method { diff --git a/src/write.rs b/src/write.rs index e56aff025..aef7808eb 100644 --- a/src/write.rs +++ b/src/write.rs @@ -1859,7 +1859,26 @@ fn write_central_directory_header(writer: &mut T, file: &ZipFileData) // file comment // - Ok(()) + 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(flag), + 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) } fn validate_extra_data(header_id: u16, data: &[u8]) -> ZipResult<()> {