//! ZIP structure validation helpers. use std::fs::{self, File}; use std::io::{Read, Seek, SeekFrom}; use std::path::Path; const EOCD_SIGNATURE: u32 = 0x0605_4b50; const ZIP64_EOCD_SIGNATURE: u32 = 0x0606_4b50; const ZIP64_EOCD_LOCATOR_SIGNATURE: u32 = 0x0706_4b50; const CENTRAL_DIRECTORY_SIGNATURE: u32 = 0x0201_4b50; const LOCAL_FILE_HEADER_SIGNATURE: u32 = 0x0403_4b50; const EOCD_MIN_LEN: u64 = 22; const EOCD_SEARCH_WINDOW: u64 = EOCD_MIN_LEN + 65_535; const ZIP64_EOCD_LOCATOR_LEN: u64 = 20; const ZIP64_EOCD_FIXED_LEN: usize = 56; const CENTRAL_DIRECTORY_FIXED_LEN: u64 = 46; const LOCAL_FILE_HEADER_FIXED_LEN: u64 = 30; const ZIP64_EXTENDED_INFORMATION_EXTRA_ID: u16 = 0x0001; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct ZipStructureReport { pub(crate) entry_count: u64, pub(crate) uses_zip64: bool, } pub(crate) fn path_has_zip_extension(path: &Path) -> bool { path.extension() .and_then(|extension| extension.to_str()) .is_some_and(|extension| extension.eq_ignore_ascii_case("zip")) } pub(crate) fn url_or_path_has_zip_extension(value: &str) -> bool { let value = value.split(['?', '#']).next().unwrap_or(value); value .rsplit('/') .next() .is_some_and(|name| name.to_ascii_lowercase().ends_with(".zip")) } pub(crate) fn validate_zip_structure(path: &Path) -> Result { let metadata = fs::metadata(path) .map_err(|error| format!("读取 ZIP 文件信息失败 {}:{error}", path.display()))?; if !metadata.is_file() { return Err(format!("ZIP 路径不是文件:{}", path.display())); } let file_len = metadata.len(); if file_len < EOCD_MIN_LEN { return Err(format!( "ZIP 文件过短,找不到 EOCD 记录 {}:{} bytes", path.display(), file_len )); } let mut file = File::open(path) .map_err(|error| format!("打开 ZIP 文件失败 {}:{error}", path.display()))?; let eocd = find_eocd(&mut file, file_len, path)?; let central_directory = read_central_directory_descriptor(&mut file, &eocd, file_len, path)?; validate_central_directory(&mut file, ¢ral_directory, file_len, path)?; Ok(ZipStructureReport { entry_count: central_directory.entry_count, uses_zip64: central_directory.uses_zip64, }) } fn find_eocd(file: &mut File, file_len: u64, path: &Path) -> Result { let tail_len = file_len.min(EOCD_SEARCH_WINDOW); let tail_start = file_len - tail_len; let mut tail = vec![0u8; tail_len as usize]; file.seek(SeekFrom::Start(tail_start)) .map_err(|error| format!("定位 ZIP EOCD 搜索窗口失败 {}:{error}", path.display()))?; file.read_exact(&mut tail) .map_err(|error| format!("读取 ZIP EOCD 搜索窗口失败 {}:{error}", path.display()))?; let min_offset = tail.len().saturating_sub(EOCD_MIN_LEN as usize); for offset in (0..=min_offset).rev() { if read_u32_le(&tail, offset)? != EOCD_SIGNATURE { continue; } let comment_len = read_u16_le(&tail, offset + 20)? as u64; let absolute_offset = tail_start + offset as u64; if absolute_offset + EOCD_MIN_LEN + comment_len != file_len { continue; } return Ok(EocdRecord { offset: absolute_offset, disk_number: read_u16_le(&tail, offset + 4)?, central_directory_disk: read_u16_le(&tail, offset + 6)?, disk_entry_count: read_u16_le(&tail, offset + 8)?, total_entry_count: read_u16_le(&tail, offset + 10)?, central_directory_size: read_u32_le(&tail, offset + 12)?, central_directory_offset: read_u32_le(&tail, offset + 16)?, }); } Err(format!("ZIP 文件缺少有效 EOCD 记录:{}", path.display())) } fn read_central_directory_descriptor( file: &mut File, eocd: &EocdRecord, file_len: u64, path: &Path, ) -> Result { if eocd.disk_number != 0 || eocd.central_directory_disk != 0 { return Err(format!("不支持多卷 ZIP 文件:{}", path.display())); } let requires_zip64 = eocd.disk_entry_count == u16::MAX || eocd.total_entry_count == u16::MAX || eocd.central_directory_size == u32::MAX || eocd.central_directory_offset == u32::MAX; if !requires_zip64 { if eocd.disk_entry_count != eocd.total_entry_count { return Err(format!( "ZIP EOCD 条目计数不一致 {}:disk={} total={}", path.display(), eocd.disk_entry_count, eocd.total_entry_count )); } return Ok(CentralDirectoryDescriptor { entry_count: eocd.total_entry_count as u64, size: eocd.central_directory_size as u64, offset: eocd.central_directory_offset as u64, suffix_offset: eocd.offset, uses_zip64: false, }); } if eocd.offset < ZIP64_EOCD_LOCATOR_LEN { return Err(format!("ZIP64 文件缺少 locator:{}", path.display())); } let locator_offset = eocd.offset - ZIP64_EOCD_LOCATOR_LEN; let locator = read_exact_at(file, locator_offset, ZIP64_EOCD_LOCATOR_LEN as usize, path)?; if read_u32_le(&locator, 0)? != ZIP64_EOCD_LOCATOR_SIGNATURE { return Err(format!("ZIP64 文件缺少有效 locator:{}", path.display())); } let eocd_disk = read_u32_le(&locator, 4)?; let zip64_eocd_offset = read_u64_le(&locator, 8)?; let total_disks = read_u32_le(&locator, 16)?; if eocd_disk != 0 || total_disks != 1 { return Err(format!("不支持多卷 ZIP64 文件:{}", path.display())); } if zip64_eocd_offset >= file_len { return Err(format!( "ZIP64 EOCD 偏移越界 {}:offset={}", path.display(), zip64_eocd_offset )); } let record = read_exact_at(file, zip64_eocd_offset, ZIP64_EOCD_FIXED_LEN, path)?; if read_u32_le(&record, 0)? != ZIP64_EOCD_SIGNATURE { return Err(format!("ZIP64 EOCD 签名无效:{}", path.display())); } let record_size = read_u64_le(&record, 4)?; if record_size < 44 { return Err(format!( "ZIP64 EOCD 长度无效 {}:{}", path.display(), record_size )); } let disk_number = read_u32_le(&record, 16)?; let central_directory_disk = read_u32_le(&record, 20)?; let disk_entry_count = read_u64_le(&record, 24)?; let total_entry_count = read_u64_le(&record, 32)?; if disk_number != 0 || central_directory_disk != 0 || disk_entry_count != total_entry_count { return Err(format!("ZIP64 EOCD 多卷或条目计数无效:{}", path.display())); } let central_directory_size = read_u64_le(&record, 40)?; let central_directory_offset = read_u64_le(&record, 48)?; Ok(CentralDirectoryDescriptor { entry_count: total_entry_count, size: central_directory_size, offset: central_directory_offset, suffix_offset: zip64_eocd_offset, uses_zip64: true, }) } fn validate_central_directory( file: &mut File, central_directory: &CentralDirectoryDescriptor, file_len: u64, path: &Path, ) -> Result<(), String> { let central_end = checked_add( central_directory.offset, central_directory.size, "ZIP central directory 边界溢出", path, )?; if central_end > central_directory.suffix_offset || central_end > file_len { return Err(format!( "ZIP central directory 越界 {}:offset={} size={} file_size={}", path.display(), central_directory.offset, central_directory.size, file_len )); } let mut cursor = central_directory.offset; for index in 0..central_directory.entry_count { if checked_add( cursor, CENTRAL_DIRECTORY_FIXED_LEN, "ZIP central header 边界溢出", path, )? > central_end { return Err(format!( "ZIP central directory 条目不足 {}:index={}", path.display(), index )); } let header = read_exact_at(file, cursor, CENTRAL_DIRECTORY_FIXED_LEN as usize, path)?; if read_u32_le(&header, 0)? != CENTRAL_DIRECTORY_SIGNATURE { return Err(format!( "ZIP central header 签名无效 {}:offset={}", path.display(), cursor )); } let compressed_size_32 = read_u32_le(&header, 20)?; let uncompressed_size_32 = read_u32_le(&header, 24)?; let file_name_len = read_u16_le(&header, 28)? as u64; let extra_len = read_u16_le(&header, 30)? as u64; let comment_len = read_u16_le(&header, 32)? as u64; let disk_start_16 = read_u16_le(&header, 34)?; let local_header_offset_32 = read_u32_le(&header, 42)?; let entry_end = checked_add( checked_add( checked_add( cursor, CENTRAL_DIRECTORY_FIXED_LEN, "ZIP central header 文件名边界溢出", path, )?, file_name_len, "ZIP central header extra 边界溢出", path, )?, extra_len + comment_len, "ZIP central header comment 边界溢出", path, )?; if entry_end > central_end { return Err(format!( "ZIP central header 条目越界 {}:offset={}", path.display(), cursor )); } if file_name_len == 0 { return Err(format!( "ZIP central header 文件名为空 {}:offset={}", path.display(), cursor )); } let name = read_exact_at( file, cursor + CENTRAL_DIRECTORY_FIXED_LEN, file_name_len as usize, path, )?; let extra = read_exact_at( file, cursor + CENTRAL_DIRECTORY_FIXED_LEN + file_name_len, extra_len as usize, path, )?; let zip64 = parse_zip64_extra( &extra, uncompressed_size_32 == u32::MAX, compressed_size_32 == u32::MAX, local_header_offset_32 == u32::MAX, disk_start_16 == u16::MAX, path, )?; let compressed_size = zip64.compressed_size.unwrap_or(compressed_size_32 as u64); let local_header_offset = zip64 .local_header_offset .unwrap_or(local_header_offset_32 as u64); let disk_start = zip64.disk_start.unwrap_or(disk_start_16 as u32); if disk_start != 0 { return Err(format!("不支持多卷 ZIP 条目:{}", path.display())); } validate_local_file_header( file, path, &name, local_header_offset, compressed_size, central_directory.offset, )?; cursor = entry_end; } if cursor != central_end { return Err(format!( "ZIP central directory 大小与条目不匹配 {}:parsed_end={} expected_end={}", path.display(), cursor, central_end )); } Ok(()) } fn validate_local_file_header( file: &mut File, path: &Path, central_name: &[u8], local_header_offset: u64, compressed_size: u64, central_directory_offset: u64, ) -> Result<(), String> { if local_header_offset >= central_directory_offset { return Err(format!( "ZIP local header 偏移越界 {}:offset={}", path.display(), local_header_offset )); } let header = read_exact_at( file, local_header_offset, LOCAL_FILE_HEADER_FIXED_LEN as usize, path, )?; if read_u32_le(&header, 0)? != LOCAL_FILE_HEADER_SIGNATURE { return Err(format!( "ZIP local header 签名无效 {}:offset={}", path.display(), local_header_offset )); } let file_name_len = read_u16_le(&header, 26)? as u64; let extra_len = read_u16_le(&header, 28)? as u64; let data_start = checked_add( checked_add( checked_add( local_header_offset, LOCAL_FILE_HEADER_FIXED_LEN, "ZIP local header 文件名边界溢出", path, )?, file_name_len, "ZIP local header extra 边界溢出", path, )?, extra_len, "ZIP local data 边界溢出", path, )?; let data_end = checked_add( data_start, compressed_size, "ZIP local data 压缩数据边界溢出", path, )?; if data_end > central_directory_offset { return Err(format!( "ZIP local data 越界 {}:data_end={} central_directory_offset={}", path.display(), data_end, central_directory_offset )); } let local_name = read_exact_at( file, local_header_offset + LOCAL_FILE_HEADER_FIXED_LEN, file_name_len as usize, path, )?; if local_name != central_name { return Err(format!( "ZIP local header 文件名与 central directory 不一致 {}:offset={}", path.display(), local_header_offset )); } Ok(()) } fn parse_zip64_extra( extra: &[u8], need_uncompressed_size: bool, need_compressed_size: bool, need_local_header_offset: bool, need_disk_start: bool, path: &Path, ) -> Result { let mut cursor = 0usize; while cursor + 4 <= extra.len() { let header_id = read_u16_le(extra, cursor)?; let data_size = read_u16_le(extra, cursor + 2)? as usize; cursor += 4; if cursor + data_size > extra.len() { return Err(format!("ZIP extra field 边界无效:{}", path.display())); } if header_id == ZIP64_EXTENDED_INFORMATION_EXTRA_ID { let data = &extra[cursor..cursor + data_size]; let values = parse_zip64_extended_information( data, need_uncompressed_size, need_compressed_size, need_local_header_offset, need_disk_start, path, )?; return Ok(values); } cursor += data_size; } if need_uncompressed_size || need_compressed_size || need_local_header_offset || need_disk_start { return Err(format!( "ZIP64 条目缺少 extended information extra:{}", path.display() )); } Ok(Zip64ExtraValues::default()) } fn parse_zip64_extended_information( data: &[u8], need_uncompressed_size: bool, need_compressed_size: bool, need_local_header_offset: bool, need_disk_start: bool, path: &Path, ) -> Result { let mut cursor = 0usize; let mut values = Zip64ExtraValues::default(); if need_uncompressed_size { values.uncompressed_size = Some(read_zip64_u64(data, &mut cursor, path)?); } if need_compressed_size { values.compressed_size = Some(read_zip64_u64(data, &mut cursor, path)?); } if need_local_header_offset { values.local_header_offset = Some(read_zip64_u64(data, &mut cursor, path)?); } if need_disk_start { if cursor + 4 > data.len() { return Err(format!("ZIP64 extra 缺少 disk start:{}", path.display())); } values.disk_start = Some(read_u32_le(data, cursor)?); } Ok(values) } fn read_zip64_u64(data: &[u8], cursor: &mut usize, path: &Path) -> Result { if *cursor + 8 > data.len() { return Err(format!("ZIP64 extra 字段长度不足:{}", path.display())); } let value = read_u64_le(data, *cursor)?; *cursor += 8; Ok(value) } fn read_exact_at(file: &mut File, offset: u64, len: usize, path: &Path) -> Result, String> { file.seek(SeekFrom::Start(offset)).map_err(|error| { format!( "定位 ZIP 文件失败 {}:offset={offset}:{error}", path.display() ) })?; let mut data = vec![0u8; len]; file.read_exact(&mut data).map_err(|error| { format!( "读取 ZIP 文件失败 {}:offset={offset} len={len}:{error}", path.display() ) })?; Ok(data) } fn checked_add(left: u64, right: u64, label: &str, path: &Path) -> Result { left.checked_add(right) .ok_or_else(|| format!("{label}:{}", path.display())) } fn read_u16_le(bytes: &[u8], offset: usize) -> Result { let end = offset .checked_add(2) .ok_or_else(|| "读取 u16 偏移溢出".to_string())?; let slice = bytes .get(offset..end) .ok_or_else(|| "读取 u16 越界".to_string())?; Ok(u16::from_le_bytes([slice[0], slice[1]])) } fn read_u32_le(bytes: &[u8], offset: usize) -> Result { let end = offset .checked_add(4) .ok_or_else(|| "读取 u32 偏移溢出".to_string())?; let slice = bytes .get(offset..end) .ok_or_else(|| "读取 u32 越界".to_string())?; Ok(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]])) } fn read_u64_le(bytes: &[u8], offset: usize) -> Result { let end = offset .checked_add(8) .ok_or_else(|| "读取 u64 偏移溢出".to_string())?; let slice = bytes .get(offset..end) .ok_or_else(|| "读取 u64 越界".to_string())?; Ok(u64::from_le_bytes([ slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7], ])) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct EocdRecord { offset: u64, disk_number: u16, central_directory_disk: u16, disk_entry_count: u16, total_entry_count: u16, central_directory_size: u32, central_directory_offset: u32, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct CentralDirectoryDescriptor { entry_count: u64, size: u64, offset: u64, suffix_offset: u64, uses_zip64: bool, } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] struct Zip64ExtraValues { uncompressed_size: Option, compressed_size: Option, local_header_offset: Option, disk_start: Option, } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; fn empty_zip() -> Vec { vec![ 0x50, 0x4b, 0x05, 0x06, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ] } fn one_file_zip(name: &[u8], data: &[u8]) -> Vec { let mut bytes = Vec::new(); bytes.extend_from_slice(&LOCAL_FILE_HEADER_SIGNATURE.to_le_bytes()); bytes.extend_from_slice(&20u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&(data.len() as u32).to_le_bytes()); bytes.extend_from_slice(&(data.len() as u32).to_le_bytes()); bytes.extend_from_slice(&(name.len() as u16).to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(name); bytes.extend_from_slice(data); let central_offset = bytes.len() as u32; bytes.extend_from_slice(&CENTRAL_DIRECTORY_SIGNATURE.to_le_bytes()); bytes.extend_from_slice(&20u16.to_le_bytes()); bytes.extend_from_slice(&20u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&(data.len() as u32).to_le_bytes()); bytes.extend_from_slice(&(data.len() as u32).to_le_bytes()); bytes.extend_from_slice(&(name.len() as u16).to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(name); let central_size = bytes.len() as u32 - central_offset; bytes.extend_from_slice(&EOCD_SIGNATURE.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&1u16.to_le_bytes()); bytes.extend_from_slice(&1u16.to_le_bytes()); bytes.extend_from_slice(¢ral_size.to_le_bytes()); bytes.extend_from_slice(¢ral_offset.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes } #[test] fn accepts_empty_zip() { let temp = TempDir::new().unwrap(); let path = temp.path().join("empty.zip"); fs::write(&path, empty_zip()).unwrap(); let report = validate_zip_structure(&path).unwrap(); assert_eq!(report.entry_count, 0); assert!(!report.uses_zip64); } #[test] fn accepts_one_file_zip() { let temp = TempDir::new().unwrap(); let path = temp.path().join("one.zip"); fs::write(&path, one_file_zip(b"nested/file.txt", b"hello")).unwrap(); let report = validate_zip_structure(&path).unwrap(); assert_eq!(report.entry_count, 1); assert!(!report.uses_zip64); } #[test] fn rejects_truncated_zip() { let temp = TempDir::new().unwrap(); let path = temp.path().join("bad.zip"); let mut bytes = one_file_zip(b"file.txt", b"hello"); bytes.truncate(bytes.len() - 8); fs::write(&path, bytes).unwrap(); let error = validate_zip_structure(&path).unwrap_err(); assert!(error.contains("EOCD") || error.contains("越界")); } #[test] fn rejects_local_name_mismatch() { let temp = TempDir::new().unwrap(); let path = temp.path().join("bad-name.zip"); let mut bytes = one_file_zip(b"file.txt", b"hello"); let central_name_offset = 30 + b"file.txt".len() + b"hello".len() + 46; bytes[central_name_offset] = b'X'; fs::write(&path, bytes).unwrap(); let error = validate_zip_structure(&path).unwrap_err(); assert!(error.contains("文件名")); } }