Skip to content

Commit 26e6e08

Browse files
authored
feat: Add by_index_with_options() for ignoring encryption (#439)
1 parent 165415d commit 26e6e08

File tree

4 files changed

+94
-13
lines changed

4 files changed

+94
-13
lines changed

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
#![allow(unexpected_cfgs)] // Needed for cfg(fuzzing) on nightly as of 2024-05-06
3535
pub use crate::compression::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS};
3636
pub use crate::read::HasZipMetadata;
37-
pub use crate::read::ZipArchive;
37+
pub use crate::read::{ZipArchive, ZipReadOptions};
3838
pub use crate::spec::{ZIP64_BYTES_THR, ZIP64_ENTRY_THR};
3939
pub use crate::types::{AesMode, DateTime};
4040
pub use crate::write::ZipWriter;

src/read.rs

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,14 +1053,16 @@ impl<R: Read + Seek> ZipArchive<R> {
10531053
) -> ZipResult<ZipFile<'_, R>> {
10541054
self.index_for_path(path)
10551055
.ok_or(ZipError::FileNotFound)
1056-
.and_then(|index| self.by_index_with_optional_password(index, Some(password)))
1056+
.and_then(|index| {
1057+
self.by_index_with_options(index, ZipReadOptions::new().password(Some(password)))
1058+
})
10571059
}
10581060

10591061
/// Search for a file entry by path
10601062
pub fn by_path<T: AsRef<Path>>(&mut self, path: T) -> ZipResult<ZipFile<'_, R>> {
10611063
self.index_for_path(path)
10621064
.ok_or(ZipError::FileNotFound)
1063-
.and_then(|index| self.by_index_with_optional_password(index, None))
1065+
.and_then(|index| self.by_index_with_options(index, ZipReadOptions::new()))
10641066
}
10651067

10661068
/// Get the index of a file entry by path, if it's present.
@@ -1116,7 +1118,7 @@ impl<R: Read + Seek> ZipArchive<R> {
11161118
let Some(index) = self.shared.files.get_index_of(name) else {
11171119
return Err(ZipError::FileNotFound);
11181120
};
1119-
self.by_index_with_optional_password(index, password)
1121+
self.by_index_with_options(index, ZipReadOptions::new().password(password))
11201122
}
11211123

11221124
/// Get a contained file by index, decrypt with given password
@@ -1137,12 +1139,12 @@ impl<R: Read + Seek> ZipArchive<R> {
11371139
file_number: usize,
11381140
password: &[u8],
11391141
) -> ZipResult<ZipFile<'_, R>> {
1140-
self.by_index_with_optional_password(file_number, Some(password))
1142+
self.by_index_with_options(file_number, ZipReadOptions::new().password(Some(password)))
11411143
}
11421144

11431145
/// Get a contained file by index
11441146
pub fn by_index(&mut self, file_number: usize) -> ZipResult<ZipFile<'_, R>> {
1145-
self.by_index_with_optional_password(file_number, None)
1147+
self.by_index_with_options(file_number, ZipReadOptions::new())
11461148
}
11471149

11481150
/// Get a contained file by index without decompressing it
@@ -1159,25 +1161,36 @@ impl<R: Read + Seek> ZipArchive<R> {
11591161
})
11601162
}
11611163

1162-
fn by_index_with_optional_password(
1164+
/// Get a contained file by index with options.
1165+
pub fn by_index_with_options(
11631166
&mut self,
11641167
file_number: usize,
1165-
mut password: Option<&[u8]>,
1168+
mut options: ZipReadOptions<'_>,
11661169
) -> ZipResult<ZipFile<'_, R>> {
11671170
let (_, data) = self
11681171
.shared
11691172
.files
11701173
.get_index(file_number)
11711174
.ok_or(ZipError::FileNotFound)?;
11721175

1173-
match (password, data.encrypted) {
1174-
(None, true) => return Err(ZipError::UnsupportedArchive(ZipError::PASSWORD_REQUIRED)),
1175-
(Some(_), false) => password = None, //Password supplied, but none needed! Discard.
1176-
_ => {}
1176+
if options.ignore_encryption_flag {
1177+
// Always use no password when we're ignoring the encryption flag.
1178+
options.password = None;
1179+
} else {
1180+
// Require and use the password only if the file is encrypted.
1181+
match (options.password, data.encrypted) {
1182+
(None, true) => {
1183+
return Err(ZipError::UnsupportedArchive(ZipError::PASSWORD_REQUIRED))
1184+
}
1185+
// Password supplied, but none needed! Discard.
1186+
(Some(_), false) => options.password = None,
1187+
_ => {}
1188+
}
11771189
}
11781190
let limit_reader = find_content(data, &mut self.reader)?;
11791191

1180-
let crypto_reader = make_crypto_reader(data, limit_reader, password, data.aes_mode)?;
1192+
let crypto_reader =
1193+
make_crypto_reader(data, limit_reader, options.password, data.aes_mode)?;
11811194

11821195
Ok(ZipFile {
11831196
data: Cow::Borrowed(data),
@@ -1555,6 +1568,38 @@ pub trait HasZipMetadata {
15551568
fn get_metadata(&self) -> &ZipFileData;
15561569
}
15571570

1571+
/// Options for reading a file from an archive.
1572+
#[derive(Default)]
1573+
pub struct ZipReadOptions<'a> {
1574+
/// The password to use when decrypting the file. This is ignored if not required.
1575+
password: Option<&'a [u8]>,
1576+
1577+
/// Ignore the value of the encryption flag and proceed as if the file were plaintext.
1578+
ignore_encryption_flag: bool,
1579+
}
1580+
1581+
impl<'a> ZipReadOptions<'a> {
1582+
/// Create a new set of options with the default values.
1583+
#[must_use]
1584+
pub fn new() -> Self {
1585+
Self::default()
1586+
}
1587+
1588+
/// Set the password, if any, to use. Return for chaining.
1589+
#[must_use]
1590+
pub fn password(mut self, password: Option<&'a [u8]>) -> Self {
1591+
self.password = password;
1592+
self
1593+
}
1594+
1595+
/// Set the ignore encryption flag. Return for chaining.
1596+
#[must_use]
1597+
pub fn ignore_encryption_flag(mut self, ignore: bool) -> Self {
1598+
self.ignore_encryption_flag = ignore;
1599+
self
1600+
}
1601+
}
1602+
15581603
/// Methods for retrieving information on zip files
15591604
impl<'a, R: Read> ZipFile<'a, R> {
15601605
pub(crate) fn take_raw_reader(&mut self) -> io::Result<io::Take<&'a mut R>> {
@@ -2065,6 +2110,7 @@ fn generate_chrono_datetime(datetime: &DateTime) -> Option<chrono::NaiveDateTime
20652110

20662111
#[cfg(test)]
20672112
mod test {
2113+
use crate::read::ZipReadOptions;
20682114
use crate::result::ZipResult;
20692115
use crate::write::SimpleFileOptions;
20702116
use crate::CompressionMethod::Stored;
@@ -2374,4 +2420,23 @@ mod test {
23742420
}
23752421
Ok(())
23762422
}
2423+
2424+
#[test]
2425+
fn test_ignore_encryption_flag() -> ZipResult<()> {
2426+
let mut reader = ZipArchive::new(Cursor::new(include_bytes!(
2427+
"../tests/data/ignore_encryption_flag.zip"
2428+
)))?;
2429+
2430+
// Get the file entry by ignoring its encryption flag.
2431+
let mut file =
2432+
reader.by_index_with_options(0, ZipReadOptions::new().ignore_encryption_flag(true))?;
2433+
let mut contents = String::new();
2434+
assert_eq!(file.name(), "plaintext.txt");
2435+
2436+
// The file claims it is encrypted, but it is not.
2437+
assert!(file.encrypted());
2438+
file.read_to_string(&mut contents)?; // ensures valid UTF-8
2439+
assert_eq!(contents, "This file is not encrypted.\n");
2440+
Ok(())
2441+
}
23772442
}
204 Bytes
Binary file not shown.

tests/zip_crypto.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,22 @@ fn encrypted_file() {
101101
file.read_to_end(&mut data).unwrap();
102102
assert_eq!(data, "abcdefghijklmnopqrstuvwxyz123456789".as_bytes());
103103
}
104+
105+
// Again, but with the options API.
106+
{
107+
use zip::read::ZipReadOptions;
108+
109+
// Correct password, read contents
110+
let mut file = archive
111+
.by_index_with_options(0, ZipReadOptions::new().password(Some("test".as_bytes())))
112+
.unwrap();
113+
let file_name = file.enclosed_name().unwrap();
114+
assert_eq!(file_name, std::path::PathBuf::from("test.txt"));
115+
116+
let mut data = Vec::new();
117+
file.read_to_end(&mut data).unwrap();
118+
assert_eq!(data, "abcdefghijklmnopqrstuvwxyz123456789".as_bytes());
119+
}
104120
}
105121

106122
#[test]

0 commit comments

Comments
 (0)