mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 13:26:23 +08:00
feat: validate official zip structure
This commit is contained in:
@@ -0,0 +1,688 @@
|
||||
//! 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<ZipStructureReport, String> {
|
||||
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<EocdRecord, String> {
|
||||
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<CentralDirectoryDescriptor, String> {
|
||||
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<Zip64ExtraValues, String> {
|
||||
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<Zip64ExtraValues, String> {
|
||||
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<u64, String> {
|
||||
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<Vec<u8>, 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<u64, String> {
|
||||
left.checked_add(right)
|
||||
.ok_or_else(|| format!("{label}:{}", path.display()))
|
||||
}
|
||||
|
||||
fn read_u16_le(bytes: &[u8], offset: usize) -> Result<u16, String> {
|
||||
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<u32, String> {
|
||||
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<u64, String> {
|
||||
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<u64>,
|
||||
compressed_size: Option<u64>,
|
||||
local_header_offset: Option<u64>,
|
||||
disk_start: Option<u32>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn empty_zip() -> Vec<u8> {
|
||||
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<u8> {
|
||||
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("文件名"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user