Merge pull request #200 from qu1x/support-extra-field
This commit is contained in:
commit
3fd44ffd5d
6 changed files with 519 additions and 48 deletions
|
@ -27,6 +27,7 @@ walkdir = "2"
|
|||
deflate = ["flate2/rust_backend"]
|
||||
deflate-miniz = ["flate2/default"]
|
||||
deflate-zlib = ["flate2/zlib"]
|
||||
unreserved = []
|
||||
default = ["bzip2", "deflate", "time"]
|
||||
|
||||
[[bench]]
|
||||
|
|
21
src/read.rs
21
src/read.rs
|
@ -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),
|
||||
}
|
||||
|
|
22
src/spec.rs
22
src/spec.rs
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
18
src/types.rs
18
src/types.rs
|
@ -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(),
|
||||
|
|
477
src/write.rs
477
src/write.rs
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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";
|
||||
|
|
Loading…
Add table
Reference in a new issue