Skip to content

Commit

Permalink
Fix io error when extracing zip archive containing folders
Browse files Browse the repository at this point in the history
If a binary is contained in a folder in a zip archive (i.e. `bin_path_in_archive` has a prefix),
extracting will fail with an io error "No such file or directory", because `fs::File::create` in
`Extract::extract_into` and `Extract::extract_path` is passed the path of the binary including the
zipped parent folder, which does not exist yet.

The commit adds tests for these cases and cleans up some existing tests.
  • Loading branch information
t-mw committed Feb 20, 2021
1 parent 38488da commit 97787bf
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 138 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [unreleased]
### Added
### Changed
- Fix io error triggered when updating binary contained in a zipped folder.
### Removed

## [0.24.0]
Expand Down
285 changes: 147 additions & 138 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,8 +415,17 @@ impl<'a> Extract<'a> {
let mut archive = zip::ZipArchive::new(source)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let path = into_dir.join(file.name());
let mut output = fs::File::create(path)?;

let output_path = into_dir.join(file.name());
if let Some(parent_dir) = output_path.parent() {
if let Err(e) = fs::create_dir_all(parent_dir) {
if e.kind() != io::ErrorKind::AlreadyExists {
return Err(Error::Io(e));
}
}
}

let mut output = fs::File::create(output_path)?;
io::copy(&mut file, &mut output)?;
}
}
Expand Down Expand Up @@ -510,7 +519,17 @@ impl<'a> Extract<'a> {
ArchiveKind::Zip => {
let mut archive = zip::ZipArchive::new(source)?;
let mut file = archive.by_name(file_to_extract.to_str().unwrap())?;
let mut output = fs::File::create(into_dir.join(file.name()))?;

let output_path = into_dir.join(file.name());
if let Some(parent_dir) = output_path.parent() {
if let Err(e) = fs::create_dir_all(parent_dir) {
if e.kind() != io::ErrorKind::AlreadyExists {
return Err(Error::Io(e));
}
}
}

let mut output = fs::File::create(output_path)?;
io::copy(&mut file, &mut output)?;
}
};
Expand Down Expand Up @@ -856,54 +875,11 @@ mod tests {
#[cfg(all(feature = "archive-tar", feature = "compression-flate2"))]
#[test]
fn unpack_tar_gzip() {
let tmp_dir = tempfile::Builder::new()
.prefix("self_update_unpack_tar_gzip_src")
.tempdir()
.expect("tempdir fail");
let tmp_path = tmp_dir.path();

let archive_src = tmp_path.join("src_archive");
fs::create_dir_all(&archive_src).expect("tmp archive-dir create fail");

let fp = archive_src.join("temp.txt");
let mut tmp_file = File::create(&fp).expect("temp file create fail");
tmp_file.write_all(b"This is a test!").unwrap();
tmp_file.sync_all().expect("sync fail");

let fp2 = archive_src.join("temp2.txt");
let mut tmp_file = File::create(&fp2).expect("temp file 2 create fail");
tmp_file.write_all(b"This is a second test!").unwrap();
tmp_file.sync_all().expect("sync fail");

let mut ar = tar::Builder::new(vec![]);
ar.append_dir_all("inner_archive", &archive_src)
.expect("tar append dir all fail");
let tar_writer = ar.into_inner().expect("failed getting tar writer");

let archive_fp = tmp_path.with_file_name("archive_file.tar.gz");
let mut archive_file = File::create(&archive_fp).expect("failed creating archive file");
let mut e = GzEncoder::new(&mut archive_file, flate2::Compression::default());
io::copy(&mut tar_writer.as_slice(), &mut e)
.expect("failed writing from tar archive to gz encoder");
e.finish().expect("gz finish fail");
archive_file.sync_all().expect("sync fail");

let out_tmp = tempfile::Builder::new()
.prefix("self_update_unpack_tar_gzip_outdir")
.tempdir()
.expect("tempdir fail");
let out_path = out_tmp.path();
Extract::from_source(&archive_fp)
.extract_into(&out_path)
.expect("extract fail");

let out_file = out_path.join("inner_archive/temp.txt");
assert!(out_file.exists());
cmp_content(&out_file, "This is a test!");

let out_file = out_path.join("inner_archive/temp2.txt");
assert!(out_file.exists());
cmp_content(&out_file, "This is a second test!");
test_extract_into(
"self_update_unpack_tar_gzip_src",
"archive.tar.gz",
ArchiveKind::Tar(Some(Compression::Gz)),
);
}

#[cfg(not(feature = "compression-flate2"))]
Expand Down Expand Up @@ -947,42 +923,11 @@ mod tests {
#[cfg(all(feature = "archive-tar", feature = "compression-flate2"))]
#[test]
fn unpack_file_tar_gzip() {
let tmp_dir = tempfile::Builder::new()
.prefix("self_update_unpack_file_tar_gzip_src")
.tempdir()
.expect("tempdir fail");
let tmp_path = tmp_dir.path();

let archive_src = tmp_path.join("src_archive");
fs::create_dir_all(&archive_src).expect("tmp archive-dir create fail");

let fp = archive_src.join("temp.txt");
let mut tmp_file = File::create(&fp).expect("temp file create fail");
tmp_file.write_all(b"This is a test!").unwrap();

let mut ar = tar::Builder::new(vec![]);
ar.append_dir_all("inner_archive", &archive_src)
.expect("tar append dir all fail");
let tar_writer = ar.into_inner().expect("failed getting tar writer");

let archive_fp = tmp_path.with_file_name("archive_file.tar.gz");
let mut archive_file = File::create(&archive_fp).expect("failed creating archive file");
let mut e = GzEncoder::new(&mut archive_file, flate2::Compression::default());
io::copy(&mut tar_writer.as_slice(), &mut e)
.expect("failed writing from tar archive to gz encoder");
e.finish().expect("gz finish fail");

let out_tmp = tempfile::Builder::new()
.prefix("self_update_unpack_file_tar_gzip_outdir")
.tempdir()
.expect("tempdir fail");
let out_path = out_tmp.path();
Extract::from_source(&archive_fp)
.extract_file(&out_path, "inner_archive/temp.txt")
.expect("extract fail");
let out_file = out_path.join("inner_archive/temp.txt");
assert!(out_file.exists());
cmp_content(&out_file, "This is a test!");
test_extract_file(
"self_update_unpack_file_tar_gzip_src",
"archive.tar.gz",
ArchiveKind::Tar(Some(Compression::Gz)),
);
}

#[cfg(not(feature = "archive-zip"))]
Expand All @@ -994,84 +939,148 @@ mod tests {
#[cfg(feature = "archive-zip")]
#[test]
fn unpack_zip() {
test_extract_into(
"self_update_unpack_zip_src",
"archive.zip",
ArchiveKind::Zip,
);
}

#[cfg(not(feature = "archive-zip"))]
#[test]
#[ignore]
fn unpack_zip_file() {
println!("WARNING: Please enable 'archive-zip' feature!");
}
#[cfg(feature = "archive-zip")]
#[test]
fn unpack_zip_file() {
test_extract_file(
"self_update_unpack_zip_src",
"archive.zip",
ArchiveKind::Zip,
);
}

fn test_extract_into(tmpfile_prefix: &str, src_archive_path: &str, archive_kind: ArchiveKind) {
let tmp_dir = tempfile::Builder::new()
.prefix("self_update_unpack_zip_src")
.prefix(tmpfile_prefix)
.tempdir()
.expect("tempdir fail");
.expect("Failed to create temp dir");

let tmp_path = tmp_dir.path();
let archive_file_path = tmp_path.join(src_archive_path);
let archive_file = File::create(&archive_file_path).expect("Failed to create archive file");

let archive_path = tmp_path.join("archive.zip");
let archive_file = File::create(&archive_path).expect("create file fail");
let mut zip = zip::ZipWriter::new(archive_file);
let options =
zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip.start_file("zipped.txt", options)
.expect("failed starting zip file");
zip.write_all(b"This is a test!")
.expect("failed writing to zip");
zip.start_file("zipped2.txt", options)
.expect("failed starting second zip file");
zip.write_all(b"This is a second test!")
.expect("failed writing to second zip");
zip.finish().expect("failed finishing zip");
build_test_archive(archive_file, &archive_file_path, archive_kind);

let out_tmp = tempfile::Builder::new()
.prefix("self_update_unpack_zip_outdir")
.prefix(&format!("{}_outdir", tmpfile_prefix))
.tempdir()
.expect("tempdir fail");
let out_path = out_tmp.path();
Extract::from_source(&archive_path)

Extract::from_source(&archive_file_path)
.extract_into(&out_path)
.expect("extract fail");
let out_file = out_path.join("zipped.txt");

let out_file = out_path.join("temp.txt");
assert!(out_file.exists());
cmp_content(&out_file, "This is a test!");

let out_file2 = out_path.join("zipped2.txt");
assert!(out_file2.exists());
cmp_content(&out_file2, "This is a second test!");
let out_file = out_path.join("inner_archive/temp2.txt");
assert!(out_file.exists());
cmp_content(&out_file, "This is a second test!");
}

#[cfg(not(feature = "archive-zip"))]
#[test]
#[ignore]
fn unpack_zip_file() {
println!("WARNING: Please enable 'archive-zip' feature!");
}
#[cfg(feature = "archive-zip")]
#[test]
fn unpack_zip_file() {
fn test_extract_file(tmpfile_prefix: &str, src_archive_path: &str, archive_kind: ArchiveKind) {
let tmp_dir = tempfile::Builder::new()
.prefix("self_update_unpack_zip_src")
.prefix(tmpfile_prefix)
.tempdir()
.expect("tempdir fail");
.expect("Failed to create temp dir");

let tmp_path = tmp_dir.path();
let archive_file_path = tmp_path.join(src_archive_path);
let archive_file = File::create(&archive_file_path).expect("Failed to create archive file");

let archive_path = tmp_path.join("archive.zip");
let archive_file = File::create(&archive_path).expect("create file fail");
let mut zip = zip::ZipWriter::new(archive_file);
let options =
zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip.start_file("zipped.txt", options)
.expect("failed starting zip file");
zip.write_all(b"This is a test!")
.expect("failed writing to zip");
zip.start_file("zipped2.txt", options)
.expect("failed starting second zip file");
zip.write_all(b"This is a second test!")
.expect("failed writing to second zip");
zip.finish().expect("failed finishing zip");
build_test_archive(archive_file, &archive_file_path, archive_kind);

let out_tmp = tempfile::Builder::new()
.prefix("self_update_unpack_zip_outdir")
.prefix(&format!("{}_outdir", tmpfile_prefix))
.tempdir()
.expect("tempdir fail");
let out_path = out_tmp.path();
Extract::from_source(&archive_path)
.extract_file(&out_path, "zipped2.txt")

Extract::from_source(&archive_file_path)
.extract_file(&out_path, "temp.txt")
.expect("extract fail");
let out_file = out_path.join("temp.txt");
assert!(out_file.exists());
cmp_content(&out_file, "This is a test!");

Extract::from_source(&archive_file_path)
.extract_file(&out_path, "inner_archive/temp2.txt")
.expect("extract fail");
let out_file = out_path.join("zipped2.txt");
let out_file = out_path.join("inner_archive/temp2.txt");
assert!(out_file.exists());
cmp_content(&out_file, "This is a second test!");
}

fn build_test_archive<T: AsRef<Path>>(
mut archive_file: fs::File,
archive_file_path: T,
archive_kind: ArchiveKind,
) {
let archive_file_path = archive_file_path.as_ref();

match archive_kind {
#[cfg(all(feature = "archive-tar", feature = "compression-flate2"))]
ArchiveKind::Tar(Some(Compression::Gz)) => {
let tmp_tar_path = archive_file_path
.parent()
.expect("Missing archive file path parent")
.join("tar_contents");
let tmp_tar_inner_path = tmp_tar_path.join("inner_archive");
fs::create_dir_all(&tmp_tar_inner_path).expect("Failed to create temp tar path");

let fp = tmp_tar_path.join("temp.txt");
let mut tmp_file = File::create(&fp).expect("temp file create fail");
tmp_file.write_all(b"This is a test!").unwrap();

let fp = tmp_tar_inner_path.join("temp2.txt");
let mut tmp_file = File::create(&fp).expect("temp file create fail");
tmp_file.write_all(b"This is a second test!").unwrap();

let mut ar = tar::Builder::new(vec![]);
ar.append_dir_all(".", &tmp_tar_path)
.expect("tar append dir all fail");
let tar_writer = ar.into_inner().expect("failed getting tar writer");

let mut e = GzEncoder::new(&mut archive_file, flate2::Compression::default());
io::copy(&mut tar_writer.as_slice(), &mut e)
.expect("failed writing from tar archive to gz encoder");
e.finish().expect("gz finish fail");
}

#[cfg(feature = "archive-zip")]
ArchiveKind::Zip => {
let mut zip = zip::ZipWriter::new(archive_file);
let options = zip::write::FileOptions::default()
.compression_method(zip::CompressionMethod::Stored);
zip.start_file("temp.txt", options)
.expect("failed starting zip file");
zip.write_all(b"This is a test!")
.expect("failed writing to zip");
zip.start_file("inner_archive/temp2.txt", options)
.expect("failed starting second zip file");
zip.write_all(b"This is a second test!")
.expect("failed writing to second zip");
zip.finish().expect("failed finishing zip");
}

_ => {
unimplemented!("{:?} not handled", archive_kind);
}
}
}
}

0 comments on commit 97787bf

Please sign in to comment.