Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ZipWriter::finish_into_readable() #75

Merged
merged 2 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 40 additions & 9 deletions src/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
pub(crate) names_map: super::HashMap<Box<str>, usize>,
pub(super) offset: u64,
pub(super) dir_start: u64,
pub(super) dir_end: u64,
}

/// ZIP archive reader
Expand Down Expand Up @@ -331,6 +330,39 @@
pub(crate) disk_with_central_directory: u32,
}

impl<R> ZipArchive<R> {
pub(crate) fn from_finalized_writer(
files: Vec<ZipFileData>,
comment: Vec<u8>,
reader: R,
central_start: u64,
) -> ZipResult<Self> {
if files.is_empty() {
return Err(ZipError::InvalidArchive(
"attempt to finalize empty zip writer into readable",
));
}
/* This is where the whole file starts. */
let initial_offset = files.first().unwrap().header_start;
let names_map: HashMap<Box<str>, usize> = files
.iter()
.enumerate()
.map(|(i, d)| (d.file_name.clone(), i))
.collect();
let shared = Arc::new(zip_archive::Shared {
files: files.into_boxed_slice(),
names_map,
offset: initial_offset,
dir_start: central_start,
});
Ok(Self {
reader,
shared,
comment: comment.into_boxed_slice().into(),
})
}
}

impl<R: Read + Seek> ZipArchive<R> {
fn get_directory_info_zip32(
footer: &spec::CentralDirectoryEnd,
Expand Down Expand Up @@ -482,11 +514,12 @@
result.and_then(|dir_info| {
// If the parsed number of files is greater than the offset then
// something fishy is going on and we shouldn't trust number_of_files.
let file_capacity = if dir_info.number_of_files > cde_start_pos as usize {
0
} else {
dir_info.number_of_files
};
let file_capacity =
if dir_info.number_of_files > dir_info.directory_start as usize {
0
} else {
dir_info.number_of_files
};
let mut files = Vec::with_capacity(file_capacity);
let mut names_map = HashMap::with_capacity(file_capacity);
reader.seek(io::SeekFrom::Start(dir_info.directory_start))?;
Expand All @@ -495,7 +528,6 @@
names_map.insert(file.file_name.clone(), files.len());
files.push(file);
}
let dir_end = reader.seek(io::SeekFrom::Start(dir_info.directory_start))?;
if dir_info.disk_number != dir_info.disk_with_central_directory {
unsupported_zip_error("Support for multi-disk files is not implemented")
} else {
Expand All @@ -504,7 +536,6 @@
names_map,
offset: dir_info.archive_offset,
dir_start: dir_info.directory_start,
dir_end,
})
}
})
Expand All @@ -524,7 +555,7 @@
}
let shared = ok_results
.into_iter()
.max_by_key(|shared| shared.dir_end)
.max_by_key(|shared| shared.dir_start)
.unwrap();
reader.seek(io::SeekFrom::Start(shared.dir_start))?;
Ok(shared)
Expand Down Expand Up @@ -1003,7 +1034,7 @@
/// `foo/../bar` as `foo/bar` (instead of `bar`). Because of this,
/// [`ZipFile::enclosed_name`] is the better option in most scenarios.
///
/// [`ParentDir`]: `Component::ParentDir`

Check warning on line 1037 in src/read.rs

View workflow job for this annotation

GitHub Actions / style_and_docs

unresolved link to `Component::ParentDir`
pub fn mangled_name(&self) -> PathBuf {
self.data.file_name_sanitized()
}
Expand Down
44 changes: 39 additions & 5 deletions src/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,39 @@ impl<A: Read + Write + Seek> ZipWriter<A> {
) -> ZipResult<()> {
self.deep_copy_file(&path_to_string(src_path), &path_to_string(dest_path))
}

/// Write the zip file into the backing stream, then produce a readable archive of that data.
///
/// This method avoids parsing the central directory records at the end of the stream for
/// a slight performance improvement over running [`ZipArchive::new()`] on the output of
/// [`Self::finish()`].
///
///```
/// # fn main() -> Result<(), zip::result::ZipError> {
/// use std::io::{Cursor, prelude::*};
/// use zip::{ZipArchive, ZipWriter, write::SimpleFileOptions};
///
/// let buf = Cursor::new(Vec::new());
/// let mut zip = ZipWriter::new(buf);
/// let options = SimpleFileOptions::default();
/// zip.start_file("a.txt", options)?;
/// zip.write_all(b"hello\n")?;
///
/// let mut zip = zip.finish_into_readable()?;
/// let mut s: String = String::new();
/// zip.by_name("a.txt")?.read_to_string(&mut s)?;
/// assert_eq!(s, "hello\n");
/// # Ok(())
/// # }
///```
pub fn finish_into_readable(mut self) -> ZipResult<ZipArchive<A>> {
let central_start = self.finalize()?;
let inner = mem::replace(&mut self.inner, Closed).unwrap();
let comment = mem::take(&mut self.comment);
let files = mem::take(&mut self.files);
let archive = ZipArchive::from_finalized_writer(files, comment, inner, central_start)?;
Ok(archive)
}
}

impl<W: Write + Seek> ZipWriter<W> {
Expand Down Expand Up @@ -1100,7 +1133,7 @@ impl<W: Write + Seek> ZipWriter<W> {
/// This will return the writer, but one should normally not append any data to the end of the file.
/// Note that the zipfile will also be finished on drop.
pub fn finish(&mut self) -> ZipResult<W> {
self.finalize()?;
let _central_start = self.finalize()?;
let inner = mem::replace(&mut self.inner, Closed);
Ok(inner.unwrap())
}
Expand Down Expand Up @@ -1160,10 +1193,10 @@ impl<W: Write + Seek> ZipWriter<W> {
self.add_symlink(path_to_string(path), path_to_string(target), options)
}

fn finalize(&mut self) -> ZipResult<()> {
fn finalize(&mut self) -> ZipResult<u64> {
self.finish_file()?;

{
let central_start = {
let central_start = self.write_central_and_footer()?;
let writer = self.inner.get_plain();
let footer_end = writer.stream_position()?;
Expand All @@ -1175,9 +1208,10 @@ impl<W: Write + Seek> ZipWriter<W> {
writer.seek(SeekFrom::End(-(central_and_footer_size as i64)))?;
self.write_central_and_footer()?;
}
}
central_start
};

Ok(())
Ok(central_start)
}

fn write_central_and_footer(&mut self) -> Result<u64, ZipError> {
Expand Down
Loading