Merge pull request #200 from qu1x/support-extra-field

This commit is contained in:
Marli Frost 2021-05-11 11:16:35 +01:00
commit 3fd44ffd5d
No known key found for this signature in database
GPG key ID: CB0BEA7CF9BD1245
6 changed files with 519 additions and 48 deletions

View file

@ -27,6 +27,7 @@ walkdir = "2"
deflate = ["flate2/rust_backend"]
deflate-miniz = ["flate2/default"]
deflate-zlib = ["flate2/zlib"]
unreserved = []
default = ["bzip2", "deflate", "time"]
[[bench]]

View file

@ -592,14 +592,16 @@ pub(crate) fn central_header_to_zip_file<R: Read + io::Seek>(
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<R: Read + io::Seek>(
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::<LittleEndian>()?;
let len = reader.read_u16::<LittleEndian>()?;
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::<LittleEndian>()?;
len_left -= 8;
}
if file.compressed_size == 0xFFFFFFFF {
file.large_file = true;
file.compressed_size = reader.read_u64::<LittleEndian>()?;
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),
}

View file

@ -117,6 +117,14 @@ impl Zip64CentralDirectoryEndLocator {
number_of_disks,
})
}
pub fn write<T: Write>(&self, writer: &mut T) -> ZipResult<()> {
writer.write_u32::<LittleEndian>(ZIP64_CENTRAL_DIRECTORY_END_LOCATOR_SIGNATURE)?;
writer.write_u32::<LittleEndian>(self.disk_with_central_directory)?;
writer.write_u64::<LittleEndian>(self.end_of_central_directory_offset)?;
writer.write_u32::<LittleEndian>(self.number_of_disks)?;
Ok(())
}
}
pub struct Zip64CentralDirectoryEnd {
@ -179,4 +187,18 @@ impl Zip64CentralDirectoryEnd {
"Could not find ZIP64 central directory end",
))
}
pub fn write<T: Write>(&self, writer: &mut T) -> ZipResult<()> {
writer.write_u32::<LittleEndian>(ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE)?;
writer.write_u64::<LittleEndian>(44)?; // record size
writer.write_u16::<LittleEndian>(self.version_made_by)?;
writer.write_u16::<LittleEndian>(self.version_needed_to_extract)?;
writer.write_u32::<LittleEndian>(self.disk_number)?;
writer.write_u32::<LittleEndian>(self.disk_with_central_directory)?;
writer.write_u64::<LittleEndian>(self.number_of_files_on_this_disk)?;
writer.write_u64::<LittleEndian>(self.number_of_files)?;
writer.write_u64::<LittleEndian>(self.central_directory_size)?;
writer.write_u64::<LittleEndian>(self.central_directory_offset)?;
Ok(())
}
}

View file

@ -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<u8>,
/// Extra field usually used for storage expansion
pub extra_field: Vec<u8>,
/// 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(),

View file

@ -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<W: Write + io::Seek> {
files: Vec<ZipFileData>,
stats: ZipWriterStats,
writing_to_file: bool,
comment: Vec<u8>,
writing_to_extra_field: bool,
writing_to_central_extra_field_only: bool,
writing_raw: bool,
comment: Vec<u8>,
}
#[derive(Default)]
@ -91,6 +93,7 @@ pub struct FileOptions {
compression_method: CompressionMethod,
last_modified_time: DateTime,
permissions: Option<u32>,
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<W: Write + io::Seek> Write for ZipWriter<W> {
}
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<A: Read + Write + io::Seek> ZipWriter<A> {
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<W: Write + io::Seek> ZipWriter<W> {
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<W: Write + io::Seek> ZipWriter<W> {
{
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<W: Write + io::Seek> ZipWriter<W> {
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<W: Write + io::Seek> ZipWriter<W> {
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<W: Write + io::Seek> ZipWriter<W> {
}
*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<W: Write + io::Seek> ZipWriter<W> {
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<S>(
&mut self,
name: S,
options: FileOptions,
align: u16,
) -> Result<u64, ZipError>
where
S: Into<String>,
{
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::<LittleEndian>(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::<LittleEndian>(0xbeef)?;
/// zip.write_u16::<LittleEndian>(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::<LittleEndian>(0xbeef)?;
/// zip.write_u16::<LittleEndian>(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::<LittleEndian>(0xdead)?;
/// zip.write_u16::<LittleEndian>(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::<LittleEndian>(0xbeef)?;
/// zip.write_u16::<LittleEndian>(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<S>(
&mut self,
name: S,
mut options: FileOptions,
) -> ZipResult<u64>
where
S: Into<String>,
{
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<u64> {
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<u64> {
// 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::<LittleEndian>(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<W: Write + io::Seek> ZipWriter<W> {
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<W: Write + io::Seek> ZipWriter<W> {
}
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<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipR
// crc-32
writer.write_u32::<LittleEndian>(file.crc32)?;
// compressed size
writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
writer.write_u32::<LittleEndian>(if file.compressed_size > 0xFFFFFFFF {
0xFFFFFFFF
} else {
file.compressed_size as u32
})?;
// uncompressed size
writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
writer.write_u32::<LittleEndian>(if file.uncompressed_size > 0xFFFFFFFF {
0xFFFFFFFF
} else {
file.uncompressed_size as u32
})?;
// file name length
writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
// extra field length
let extra_field = build_extra_field(file)?;
writer.write_u16::<LittleEndian>(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::<LittleEndian>(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<T: Write + io::Seek>(
const CRC32_OFFSET: u64 = 14;
writer.seek(io::SeekFrom::Start(file.header_start + CRC32_OFFSET))?;
writer.write_u32::<LittleEndian>(file.crc32)?;
writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
writer.write_u32::<LittleEndian>(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::<LittleEndian>(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<T: Write>(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::<LittleEndian>(spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE)?;
// version made by
@ -735,14 +999,21 @@ fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData)
// crc-32
writer.write_u32::<LittleEndian>(file.crc32)?;
// compressed size
writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
writer.write_u32::<LittleEndian>(if file.compressed_size > 0xFFFFFFFF {
0xFFFFFFFF
} else {
file.compressed_size as u32
})?;
// uncompressed size
writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
writer.write_u32::<LittleEndian>(if file.uncompressed_size > 0xFFFFFFFF {
0xFFFFFFFF
} else {
file.uncompressed_size as u32
})?;
// file name length
writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
// extra field length
let extra_field = build_extra_field(file)?;
writer.write_u16::<LittleEndian>(extra_field.len() as u16)?;
writer.write_u16::<LittleEndian>(zip64_extra_field_length + file.extra_field.len() as u16)?;
// file comment length
writer.write_u16::<LittleEndian>(0)?;
// disk number start
@ -752,21 +1023,139 @@ fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData)
// external file attributes
writer.write_u32::<LittleEndian>(file.external_attributes)?;
// relative offset of local header
writer.write_u32::<LittleEndian>(file.header_start as u32)?;
writer.write_u32::<LittleEndian>(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
// <none>
Ok(())
}
fn build_extra_field(_file: &ZipFileData) -> ZipResult<Vec<u8>> {
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::<LittleEndian>()?;
let size = data.read_u16::<LittleEndian>()? 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<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
// This entry in the Local header MUST include BOTH original
// and compressed file size fields.
writer.write_u16::<LittleEndian>(0x0001)?;
writer.write_u16::<LittleEndian>(16)?;
writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
writer.write_u64::<LittleEndian>(file.compressed_size)?;
// Excluded fields:
// u32: disk start number
Ok(())
}
fn update_local_zip64_extra_field<T: Write + io::Seek>(
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::<LittleEndian>(file.uncompressed_size)?;
writer.write_u64::<LittleEndian>(file.compressed_size)?;
// Excluded fields:
// u32: disk start number
Ok(())
}
fn write_central_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<u16> {
// 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::<LittleEndian>(0x0001)?;
writer.write_u16::<LittleEndian>(size)?;
size += 4;
if uncompressed_size {
writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
}
if compressed_size {
writer.write_u64::<LittleEndian>(file.compressed_size)?;
}
if header_start {
writer.write_u64::<LittleEndian>(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,
];

View file

@ -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<Vec<u8>>) -> 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::<LittleEndian>(0xbeef)?;
zip.write_u16::<LittleEndian>(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<Vec<u8>>) -> zip::result::ZipResult<()> {
}
fn read_zip<R: Read + Seek>(zip_file: R) -> zip::result::ZipResult<zip::ZipArchive<R>> {
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::<HashSet<_>>();
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::<LittleEndian>(0xbeef)?;
extra_data.write_u16::<LittleEndian>(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";