Skip to content

Commit c10c339

Browse files
Pr0metheanJames MacAdie (TT sandbox)github-advanced-security[bot]Its-Just-Nans
authored
doc(examples): add delete/update examples (#56)
* Add remove and update file examples Per discussion #430, it was not obvious to me how to update a file within an archive. I have added some examples of how to delete and update to make it easier for anyone in a similar situation to me in the future * Potential fix for code scanning alert no. 248: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> * Potential fix for code scanning alert no. 250: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> * Debug formatting of filenames to fix log injection Signed-off-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> * Debug formatting of filenames to fix log injection Signed-off-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> * Potential fix for code scanning alert no. 249: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> * Fix: don't drop new_archive since it's already been consumed Signed-off-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> * Fixes * cargo clippy --fix * Suppress a Clippy warning * cargo fmt --all * Simplify examples by having main return anyhow::Result * Remove CI filters that were preventing this PR from being tested * Fix: use head-ref to define PR concurrency group, so dedup still happens * cargo fmt --all * rm anyhow * Simplify zip_dir error handling with a question mark Signed-off-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> * fix return * Fix error-type conversion issue in specific configurations * fix file_info.rs --------- Signed-off-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> Co-authored-by: James MacAdie (TT sandbox) <j.macadie@ttg.co.uk> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: n4n5 <its.just.n4n5@gmail.com>
1 parent 9c1a9c7 commit c10c339

File tree

5 files changed

+220
-39
lines changed

5 files changed

+220
-39
lines changed

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ bencher = "0.1"
7373
getrandom = { version = "0.3", default-features = false, features = [] }
7474
walkdir = "2.5"
7575
time = { version = "0.3", features = ["formatting", "macros"] }
76-
anyhow = "1.0.100"
7776
clap = { version = "=4.4.18", features = ["derive"] }
7877
tempfile = "3.15"
7978
rayon = "1.11"
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// See this dicussion for further background on why it is done like this:
2+
// https://github.com/zip-rs/zip/discussions/430
3+
4+
use zip::result::{ZipError, ZipResult};
5+
6+
fn main() -> Result<(), Box<dyn std::error::Error>> {
7+
let args: Vec<_> = std::env::args().collect();
8+
if args.len() < 3 {
9+
return Err(format!(
10+
"Usage: {:?} <filename> <file_within_archive_to_delete>",
11+
args[0]
12+
)
13+
.into());
14+
}
15+
let filename = &*args[1];
16+
let file_to_remove = &*args[2];
17+
let base_dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
18+
remove_file(&base_dir, filename, file_to_remove, false)?;
19+
Ok(())
20+
}
21+
22+
fn remove_file(
23+
base_dir: &std::path::Path,
24+
archive_filename: &str,
25+
file_to_remove: &str,
26+
in_place: bool,
27+
) -> ZipResult<()> {
28+
let unsafe_path = std::path::Path::new(archive_filename);
29+
let joined = base_dir.join(unsafe_path);
30+
let fname = joined.canonicalize().map_err(|_| ZipError::FileNotFound)?;
31+
if !fname.starts_with(base_dir) {
32+
return Err(ZipError::FileNotFound);
33+
}
34+
let zipfile = std::fs::File::open(&fname)?;
35+
36+
let mut archive = zip::ZipArchive::new(zipfile)?;
37+
38+
// Open a new, empty archive for writing to
39+
let new_filename = replacement_filename(fname.as_path())?;
40+
let new_file = std::fs::File::create(&new_filename)?;
41+
let mut new_archive = zip::ZipWriter::new(new_file);
42+
43+
// Loop through the original archive:
44+
// - Skip the target file
45+
// - Copy everything else across as raw, which saves the bother of decoding it
46+
// The end effect is to have a new archive, which is a clone of the original,
47+
// save for the target file which has been omitted i.e. deleted
48+
let target: &std::path::Path = file_to_remove.as_ref();
49+
for i in 0..archive.len() {
50+
let file = archive.by_index_raw(i)?;
51+
match file.enclosed_name() {
52+
Some(p) if p == target => (),
53+
_ => new_archive.raw_copy_file(file)?,
54+
}
55+
}
56+
new_archive.finish()?;
57+
58+
drop(archive);
59+
60+
// If we're doing this in place then overwrite the original with the new
61+
if in_place {
62+
std::fs::rename(new_filename, &fname)?;
63+
}
64+
65+
Ok(())
66+
}
67+
68+
fn replacement_filename(source: &std::path::Path) -> ZipResult<std::path::PathBuf> {
69+
let mut new = std::path::PathBuf::from(source);
70+
let mut stem = source.file_stem().ok_or(ZipError::FileNotFound)?.to_owned();
71+
stem.push("_updated");
72+
new.set_file_name(stem);
73+
let ext = source.extension().ok_or(ZipError::FileNotFound)?;
74+
new.set_extension(ext);
75+
Ok(new)
76+
}

examples/file_info.rs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
1-
use anyhow::{anyhow, Context, Result};
1+
//! Get the zip file infos
2+
//!
3+
//! ```sh
4+
//! cargo run --example file_info my_file.zip
5+
//! ```
6+
//!
7+
28
use std::fs;
39
use std::io::BufReader;
410

5-
fn main() -> Result<()> {
11+
fn main() -> Result<(), Box<dyn std::error::Error>> {
612
let args: Vec<_> = std::env::args().collect();
713
if args.len() < 2 {
8-
return Err(anyhow!("Usage: {} <filename>", args[0]));
14+
return Err(format!("Usage: {} <filename>", args[0]).into());
915
}
1016
let fname_arg = &args[1];
1117
// Determine a trusted base directory (current working directory).
12-
let base_dir =
13-
std::env::current_dir().with_context(|| "Could not determine current directory")?;
18+
let base_dir = std::env::current_dir()
19+
.map_err(|e| format!("Could not determine current directory: {e}"))?;
1420
// Construct the path relative to the trusted base directory and canonicalize it.
1521
let candidate_path = base_dir.join(fname_arg);
1622
// FIXME: still vulnerable to a Time-of-check to time-of-use (TOCTOU) race condition.
@@ -21,14 +27,12 @@ fn main() -> Result<()> {
2127
// (which isn't in std).
2228
let path = candidate_path
2329
.canonicalize()
24-
.with_context(|| format!("Could not open {fname_arg:?}"))?;
25-
if !path.starts_with(&base_dir.canonicalize().unwrap_or(base_dir.clone())) {
26-
return Err(anyhow!(
27-
"Error: refusing to open path outside of base directory: {fname_arg:?}"
28-
));
30+
.map_err(|e| format!("Could not open {fname_arg:?}: {e}"))?;
31+
if !path.starts_with(base_dir.canonicalize().unwrap_or(base_dir.clone())) {
32+
return Err("Error: refusing to open path outside of base directory: {fname_arg:?}".into());
2933
}
3034
let mut archive = zip::ZipArchive::new(BufReader::new(fs::File::open(&path)?))
31-
.with_context(|| format!("Could not open {fname_arg:?}"))?;
35+
.map_err(|e| format!("Could not open {fname_arg:?}: {e}"))?;
3236
for i in 0..archive.len() {
3337
let file = archive.by_index(i)?;
3438
let outpath = match file.enclosed_name() {

examples/update_file_in_archive.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// See this dicussion for further background on why it is done like this:
2+
// https://github.com/zip-rs/zip/discussions/430
3+
4+
use std::io::Write;
5+
use zip::result::{ZipError, ZipResult};
6+
use zip::write::SimpleFileOptions;
7+
8+
fn main() -> Result<(), Box<dyn std::error::Error>> {
9+
let args: Vec<_> = std::env::args().collect();
10+
if args.len() < 3 {
11+
return Err(format!(
12+
"Usage: {:?} <filename> <file_within_archive_to_update>",
13+
args[0]
14+
)
15+
.into());
16+
}
17+
let filename = &*args[1];
18+
let file_to_update = &*args[2];
19+
update_file(filename, file_to_update, false)?;
20+
Ok(())
21+
}
22+
23+
fn update_file(archive_filename: &str, file_to_update: &str, in_place: bool) -> ZipResult<()> {
24+
// Validate the archive path:
25+
// - Disallow absolute paths.
26+
// - Disallow parent directory components.
27+
// - Ensure the resolved path stays within the current working directory.
28+
let raw_path = std::path::Path::new(archive_filename);
29+
if raw_path.is_absolute()
30+
|| raw_path
31+
.components()
32+
.any(|c| matches!(c, std::path::Component::ParentDir))
33+
{
34+
return Err(ZipError::FileNotFound);
35+
}
36+
37+
// Use the current working directory as the base for archives.
38+
let base_dir = std::env::current_dir()?;
39+
let joined = base_dir.join(raw_path);
40+
let archive_path = joined.canonicalize()?;
41+
if !archive_path.starts_with(&base_dir) {
42+
return Err(ZipError::FileNotFound);
43+
}
44+
45+
let zipfile = std::fs::File::open(&archive_path)?;
46+
47+
let mut archive = zip::ZipArchive::new(zipfile)?;
48+
49+
// Open a new, empty archive for writing to
50+
let new_filename = replacement_filename(&archive_path)?;
51+
let new_file = std::fs::File::create(&new_filename)?;
52+
let mut new_archive = zip::ZipWriter::new(new_file);
53+
54+
// Loop through the original archive:
55+
// - Write the target file from some bytes
56+
// - Copy everything else across as raw, which saves the bother of decoding it
57+
// The end effect is to have a new archive, which is a clone of the original,
58+
// save for the target file which has been re-written
59+
let target: &std::path::Path = file_to_update.as_ref();
60+
let new = b"Lorem ipsum";
61+
for i in 0..archive.len() {
62+
let file = archive.by_index_raw(i)?;
63+
match file.enclosed_name() {
64+
Some(p) if p == target => {
65+
new_archive.start_file(file_to_update, SimpleFileOptions::default())?;
66+
new_archive.write_all(new)?;
67+
new_archive.flush()?;
68+
}
69+
_ => new_archive.raw_copy_file(file)?,
70+
}
71+
}
72+
new_archive.finish()?;
73+
74+
drop(archive);
75+
76+
// If we're doing this in place then overwrite the original with the new
77+
if in_place {
78+
std::fs::rename(&new_filename, &archive_path)?;
79+
}
80+
81+
Ok(())
82+
}
83+
84+
fn replacement_filename(source: &std::path::Path) -> ZipResult<std::path::PathBuf> {
85+
let mut new = std::path::PathBuf::from(source);
86+
let mut stem = source.file_stem().ok_or(ZipError::FileNotFound)?.to_owned();
87+
stem.push("_updated");
88+
new.set_file_name(stem);
89+
let ext = source.extension().ok_or(ZipError::FileNotFound)?;
90+
new.set_extension(ext);
91+
Ok(new)
92+
}

examples/write_dir.rs

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
use anyhow::{anyhow, Result};
1+
//! Example to write a zip dir
2+
//!
3+
//! ```sh
4+
//! cargo run --example write_dir src/ dest.zip xz
5+
//! ```
6+
27
use clap::{Parser, ValueEnum};
38
use walkdir::WalkDir;
49
use zip::{cfg_if_expr, result::ZipError, write::SimpleFileOptions};
@@ -28,41 +33,46 @@ enum CompressionMethod {
2833
Zstd,
2934
}
3035

31-
fn main() -> Result<()> {
36+
fn main() -> Result<(), Box<dyn std::error::Error>> {
3237
let args = Args::parse();
3338
let src_dir = &args.source;
34-
let dst_file = &args.destination;
35-
let method: Result<zip::CompressionMethod> = match args.compression_method {
36-
CompressionMethod::Stored => Ok(zip::CompressionMethod::Stored),
37-
CompressionMethod::Deflated => cfg_if_expr! {
38-
#[cfg(feature = "_deflate-any")] => Ok(zip::CompressionMethod::Deflated),
39-
_ => Err(anyhow!("The `deflate-flate2` features are not enabled")),
40-
},
41-
CompressionMethod::Bzip2 => cfg_if_expr! {
42-
#[cfg(feature = "_bzip2_any")] => Ok(zip::CompressionMethod::Bzip2),
43-
_ => Err(anyhow!("The `bzip2` features are not enabled")),
44-
},
45-
CompressionMethod::Xz => cfg_if_expr! {
46-
#[cfg(feature = "xz")] => Ok(zip::CompressionMethod::Xz),
47-
_ => Err(anyhow!("The `xz` feature is not enabled")),
48-
},
49-
CompressionMethod::Zstd => cfg_if_expr! {
50-
#[cfg(feature = "zstd")] => Ok(zip::CompressionMethod::Zstd),
51-
_ => Err(anyhow!("The `zstd` feature is not enabled")),
52-
},
53-
};
39+
let dest_file = &args.destination;
40+
let method: Result<zip::CompressionMethod, Box<dyn std::error::Error>> =
41+
match args.compression_method {
42+
CompressionMethod::Stored => Ok(zip::CompressionMethod::Stored),
43+
CompressionMethod::Deflated => cfg_if_expr! {
44+
#[cfg(feature = "_deflate-any")] => Ok(zip::CompressionMethod::Deflated),
45+
_ => Err("The `deflate-flate2` features are not enabled".into()),
46+
},
47+
CompressionMethod::Bzip2 => cfg_if_expr! {
48+
#[cfg(feature = "_bzip2_any")] => Ok(zip::CompressionMethod::Bzip2),
49+
_ => Err("The `bzip2` features are not enabled".into()),
50+
},
51+
CompressionMethod::Xz => cfg_if_expr! {
52+
#[cfg(feature = "xz")] => Ok(zip::CompressionMethod::Xz),
53+
_ => Err("The `xz` feature is not enabled".into()),
54+
},
55+
CompressionMethod::Zstd => cfg_if_expr! {
56+
#[cfg(feature = "zstd")] => Ok(zip::CompressionMethod::Zstd),
57+
_ => Err("The `zstd` feature is not enabled".into()),
58+
},
59+
};
5460
let method = method?;
55-
zip_dir(src_dir, dst_file, method)?;
56-
println!("done: {src_dir:?} written to {dst_file:?}");
61+
zip_dir(src_dir, dest_file, method)?;
62+
println!("done: {src_dir:?} written to {dest_file:?}");
5763
Ok(())
5864
}
5965

60-
fn zip_dir(src_dir: &Path, dst_file: &Path, method: zip::CompressionMethod) -> Result<()> {
66+
fn zip_dir(
67+
src_dir: &Path,
68+
dest_file: &Path,
69+
method: zip::CompressionMethod,
70+
) -> Result<(), Box<dyn std::error::Error>> {
6171
if !Path::new(src_dir).is_dir() {
6272
return Err(ZipError::FileNotFound.into());
6373
}
6474

65-
let path = Path::new(dst_file);
75+
let path = Path::new(dest_file);
6676
let file = File::create(path)?;
6777

6878
let walkdir = WalkDir::new(src_dir);
@@ -80,7 +90,7 @@ fn zip_dir(src_dir: &Path, dst_file: &Path, method: zip::CompressionMethod) -> R
8090
let path_as_string = name
8191
.to_str()
8292
.map(str::to_owned)
83-
.ok_or_else(|| anyhow!("{name:?} is a Non UTF-8 Path"))?;
93+
.ok_or_else(|| format!("{name:?} is a Non UTF-8 Path"))?;
8494

8595
// Write file or directory explicitly
8696
// Some unzip tools unzip files with directory paths correctly, some do not!

0 commit comments

Comments
 (0)