aboutsummaryrefslogtreecommitdiff
path: root/src/write.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/write.rs')
-rw-r--r--src/write.rs180
1 files changed, 143 insertions, 37 deletions
diff --git a/src/write.rs b/src/write.rs
index 551b4e3..14252b4 100644
--- a/src/write.rs
+++ b/src/write.rs
@@ -7,6 +7,7 @@ use crate::spec;
use crate::types::{AtomicU64, DateTime, System, ZipFileData, DEFAULT_VERSION};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use crc32fast::Hasher;
+use std::convert::TryInto;
use std::default::Default;
use std::io;
use std::io::prelude::*;
@@ -110,31 +111,6 @@ pub struct FileOptions {
}
impl FileOptions {
- /// Construct a new FileOptions object
- pub fn default() -> FileOptions {
- FileOptions {
- #[cfg(any(
- feature = "deflate",
- feature = "deflate-miniz",
- feature = "deflate-zlib"
- ))]
- compression_method: CompressionMethod::Deflated,
- #[cfg(not(any(
- feature = "deflate",
- feature = "deflate-miniz",
- feature = "deflate-zlib"
- )))]
- compression_method: CompressionMethod::Stored,
- compression_level: None,
- #[cfg(feature = "time")]
- last_modified_time: DateTime::from_time(OffsetDateTime::now_utc()).unwrap_or_default(),
- #[cfg(not(feature = "time"))]
- last_modified_time: DateTime::default(),
- permissions: None,
- large_file: false,
- }
- }
-
/// Set the compression method for the new file
///
/// The default is `CompressionMethod::Deflated`. If the deflate compression feature is
@@ -174,7 +150,11 @@ impl FileOptions {
///
/// The format is represented with unix-style permissions.
/// The default is `0o644`, which represents `rw-r--r--` for files,
- /// and `0o755`, which represents `rwxr-xr-x` for directories
+ /// and `0o755`, which represents `rwxr-xr-x` for directories.
+ ///
+ /// This method only preserves the file permissions bits (via a `& 0o777`) and discards
+ /// higher file mode bits. So it cannot be used to denote an entry as a directory,
+ /// symlink, or other special file type.
#[must_use]
pub fn unix_permissions(mut self, mode: u32) -> FileOptions {
self.permissions = Some(mode & 0o777);
@@ -194,8 +174,29 @@ impl FileOptions {
}
impl Default for FileOptions {
+ /// Construct a new FileOptions object
fn default() -> Self {
- Self::default()
+ Self {
+ #[cfg(any(
+ feature = "deflate",
+ feature = "deflate-miniz",
+ feature = "deflate-zlib"
+ ))]
+ compression_method: CompressionMethod::Deflated,
+ #[cfg(not(any(
+ feature = "deflate",
+ feature = "deflate-miniz",
+ feature = "deflate-zlib"
+ )))]
+ compression_method: CompressionMethod::Stored,
+ compression_level: None,
+ #[cfg(feature = "time")]
+ last_modified_time: OffsetDateTime::now_utc().try_into().unwrap_or_default(),
+ #[cfg(not(feature = "time"))]
+ last_modified_time: DateTime::default(),
+ permissions: None,
+ large_file: false,
+ }
}
}
@@ -348,7 +349,7 @@ impl<W: Write + io::Seek> ZipWriter<W> {
{
let writer = self.inner.get_plain();
- let header_start = writer.seek(io::SeekFrom::Current(0))?;
+ let header_start = writer.stream_position()?;
let permissions = options.permissions.unwrap_or(0o100644);
let mut file = ZipFileData {
@@ -375,7 +376,7 @@ impl<W: Write + io::Seek> ZipWriter<W> {
};
write_local_file_header(writer, &file)?;
- let header_end = writer.seek(io::SeekFrom::Current(0))?;
+ let header_end = writer.stream_position()?;
self.stats.start = header_end;
*file.data_start.get_mut() = header_end;
@@ -404,7 +405,7 @@ impl<W: Write + io::Seek> ZipWriter<W> {
file.crc32 = self.stats.hasher.clone().finalize();
file.uncompressed_size = self.stats.bytes_written;
- let file_end = writer.seek(io::SeekFrom::Current(0))?;
+ let file_end = writer.stream_position()?;
file.compressed_size = file_end - self.stats.start;
update_local_file_header(writer, file)?;
@@ -723,7 +724,7 @@ impl<W: Write + io::Seek> ZipWriter<W> {
/// Add a directory entry, 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",
@@ -747,17 +748,55 @@ impl<W: Write + io::Seek> ZipWriter<W> {
Ok(inner.unwrap())
}
+ /// Add a symlink entry.
+ ///
+ /// The zip archive will contain an entry for path `name` which is a symlink to `target`.
+ ///
+ /// No validation or normalization of the paths is performed. For best results,
+ /// callers should normalize `\` to `/` and ensure symlinks are relative to other
+ /// paths within the zip archive.
+ ///
+ /// WARNING: not all zip implementations preserve symlinks on extract. Some zip
+ /// implementations may materialize a symlink as a regular file, possibly with the
+ /// content incorrectly set to the symlink target. For maximum portability, consider
+ /// storing a regular file instead.
+ pub fn add_symlink<N, T>(
+ &mut self,
+ name: N,
+ target: T,
+ mut options: FileOptions,
+ ) -> ZipResult<()>
+ where
+ N: Into<String>,
+ T: Into<String>,
+ {
+ if options.permissions.is_none() {
+ options.permissions = Some(0o777);
+ }
+ *options.permissions.as_mut().unwrap() |= 0o120000;
+ // The symlink target is stored as file content. And compressing the target path
+ // likely wastes space. So always store.
+ options.compression_method = CompressionMethod::Stored;
+
+ self.start_entry(name, options, None)?;
+ self.writing_to_file = true;
+ self.write_all(target.into().as_bytes())?;
+ self.writing_to_file = false;
+
+ Ok(())
+ }
+
fn finalize(&mut self) -> ZipResult<()> {
self.finish_file()?;
{
let writer = self.inner.get_plain();
- let central_start = writer.seek(io::SeekFrom::Current(0))?;
+ let central_start = writer.stream_position()?;
for file in self.files.iter() {
write_central_directory_header(writer, file)?;
}
- let central_size = writer.seek(io::SeekFrom::Current(0))? - central_start;
+ let central_size = writer.stream_position()? - central_start;
if self.files.len() > spec::ZIP64_ENTRY_THR
|| central_size.max(central_start) > spec::ZIP64_BYTES_THR
@@ -806,7 +845,7 @@ impl<W: Write + io::Seek> Drop for ZipWriter<W> {
fn drop(&mut self) {
if !self.inner.is_closed() {
if let Err(e) = self.finalize() {
- let _ = write!(io::stderr(), "ZipWriter drop failed: {:?}", e);
+ let _ = write!(io::stderr(), "ZipWriter drop failed: {e:?}");
}
}
}
@@ -1169,8 +1208,7 @@ fn validate_extra_data(file: &ZipFileData) -> ZipResult<()> {
return Err(ZipError::Io(io::Error::new(
io::ErrorKind::Other,
format!(
- "Extra data header ID {:#06} requires crate feature \"unreserved\"",
- kind,
+ "Extra data header ID {kind:#06} requires crate feature \"unreserved\"",
),
)));
}
@@ -1259,7 +1297,7 @@ fn path_to_string(path: &std::path::Path) -> String {
if !path_str.is_empty() {
path_str.push('/');
}
- path_str.push_str(&*os_str.to_string_lossy());
+ path_str.push_str(&os_str.to_string_lossy());
}
}
path_str
@@ -1286,6 +1324,13 @@ mod test {
}
#[test]
+ fn unix_permissions_bitmask() {
+ // unix_permissions() throws away upper bits.
+ let options = FileOptions::default().unix_permissions(0o120777);
+ assert_eq!(options.permissions, Some(0o777));
+ }
+
+ #[test]
fn write_zip_dir() {
let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
writer
@@ -1314,6 +1359,67 @@ mod test {
}
#[test]
+ fn write_symlink_simple() {
+ let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
+ writer
+ .add_symlink(
+ "name",
+ "target",
+ FileOptions::default().last_modified_time(
+ DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
+ ),
+ )
+ .unwrap();
+ assert!(writer
+ .write(b"writing to a symlink is not allowed and will not write any data")
+ .is_err());
+ let result = writer.finish().unwrap();
+ assert_eq!(result.get_ref().len(), 112);
+ assert_eq!(
+ *result.get_ref(),
+ &[
+ 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 252, 47, 111, 70, 6, 0, 0, 0,
+ 6, 0, 0, 0, 4, 0, 0, 0, 110, 97, 109, 101, 116, 97, 114, 103, 101, 116, 80, 75, 1,
+ 2, 46, 3, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 252, 47, 111, 70, 6, 0, 0, 0, 6, 0,
+ 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 161, 0, 0, 0, 0, 110, 97, 109, 101,
+ 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 50, 0, 0, 0, 40, 0, 0, 0, 0, 0
+ ] as &[u8],
+ );
+ }
+
+ #[test]
+ fn write_symlink_wonky_paths() {
+ let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
+ writer
+ .add_symlink(
+ "directory\\link",
+ "/absolute/symlink\\with\\mixed/slashes",
+ FileOptions::default().last_modified_time(
+ DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
+ ),
+ )
+ .unwrap();
+ assert!(writer
+ .write(b"writing to a symlink is not allowed and will not write any data")
+ .is_err());
+ let result = writer.finish().unwrap();
+ assert_eq!(result.get_ref().len(), 162);
+ assert_eq!(
+ *result.get_ref(),
+ &[
+ 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 95, 41, 81, 245, 36, 0, 0, 0,
+ 36, 0, 0, 0, 14, 0, 0, 0, 100, 105, 114, 101, 99, 116, 111, 114, 121, 92, 108, 105,
+ 110, 107, 47, 97, 98, 115, 111, 108, 117, 116, 101, 47, 115, 121, 109, 108, 105,
+ 110, 107, 92, 119, 105, 116, 104, 92, 109, 105, 120, 101, 100, 47, 115, 108, 97,
+ 115, 104, 101, 115, 80, 75, 1, 2, 46, 3, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 95,
+ 41, 81, 245, 36, 0, 0, 0, 36, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255,
+ 161, 0, 0, 0, 0, 100, 105, 114, 101, 99, 116, 111, 114, 121, 92, 108, 105, 110,
+ 107, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 60, 0, 0, 0, 80, 0, 0, 0, 0, 0
+ ] as &[u8],
+ );
+ }
+
+ #[test]
fn write_mimetype_zip() {
let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
let options = FileOptions {