diff --git a/src/write.rs b/src/write.rs index 58493d37..529f3e82 100644 --- a/src/write.rs +++ b/src/write.rs @@ -13,6 +13,7 @@ use crate::types::{ffi, AesVendorVersion, DateTime, System, ZipFileData, DEFAULT use core::num::NonZeroU64; use crc32fast::Hasher; use indexmap::IndexMap; +use std::borrow::ToOwned; use std::default::Default; use std::io; use std::io::prelude::*; @@ -723,14 +724,15 @@ impl ZipWriter { } /// Start a new file for with the requested options. - fn start_entry( + fn start_entry( &mut self, name: S, options: FileOptions, raw_values: Option, ) -> ZipResult<()> where - S: Into>, + S: Into> + ToOwned, + SToOwned: Into>, { self.finish_file()?; @@ -796,8 +798,8 @@ impl ZipWriter { crc32: raw_values.crc32, compressed_size: raw_values.compressed_size, uncompressed_size: raw_values.uncompressed_size, - file_name: name.into(), - file_name_raw: vec![].into_boxed_slice(), // Never used for saving + file_name: name.to_owned().into(), // Never used for saving, but used as map key in insert_file_data() + file_name_raw: name.into().bytes().collect(), extra_field, central_extra_field: options.extended_options.central_extra_data().cloned(), file_comment: String::with_capacity(0).into_boxed_str(), @@ -815,15 +817,15 @@ impl ZipWriter { let index = self.insert_file_data(file)?; let file = &mut self.files[index]; let writer = self.inner.get_plain(); + // local file header signature writer.write_u32_le(spec::LOCAL_FILE_HEADER_SIGNATURE)?; // version needed to extract writer.write_u16_le(file.version_needed())?; // general purpose bit flag - let flag = if !file.file_name.is_ascii() { - 1u16 << 11 - } else { - 0 - } | if file.encrypted { 1u16 << 0 } else { 0 }; + let is_utf8 = std::str::from_utf8(&file.file_name_raw).is_ok(); + let is_ascii = file.file_name_raw.is_ascii(); + let flag = if is_utf8 && !is_ascii { 1u16 << 11 } else { 0 } + | if file.encrypted { 1u16 << 0 } else { 0 }; writer.write_u16_le(flag)?; // Compression method #[allow(deprecated)] @@ -842,7 +844,7 @@ impl ZipWriter { writer.write_u32_le(file.uncompressed_size as u32)?; } // file name length - writer.write_u16_le(file.file_name.as_bytes().len() as u16)?; + writer.write_u16_le(file.file_name_raw.len() as u16)?; // extra field length let mut extra_field_length = file.extra_field_len(); if file.large_file { @@ -855,7 +857,7 @@ impl ZipWriter { let extra_field_length = extra_field_length as u16; writer.write_u16_le(extra_field_length)?; // file name - writer.write_all(file.file_name.as_bytes())?; + writer.write_all(&file.file_name_raw)?; // zip64 extra field if file.large_file { write_local_zip64_extra_field(writer, file)?; @@ -1053,13 +1055,14 @@ impl ZipWriter { /// same name as a file already in the archive. /// /// The data should be written using the [`Write`] implementation on this [`ZipWriter`] - pub fn start_file( + pub fn start_file( &mut self, name: S, mut options: FileOptions, ) -> ZipResult<()> where - S: Into>, + S: Into> + ToOwned, + SToOwned: Into>, { Self::normalize_options(&mut options); let make_new_self = self.inner.prepare_next_writer( @@ -1188,9 +1191,10 @@ impl ZipWriter { /// Ok(()) /// } /// ``` - pub fn raw_copy_file_rename(&mut self, mut file: ZipFile, name: S) -> ZipResult<()> + pub fn raw_copy_file_rename(&mut self, mut file: ZipFile, name: S) -> ZipResult<()> where - S: Into>, + S: Into> + ToOwned, + SToOwned: Into>, { let mut options = SimpleFileOptions::default() .large_file(file.compressed_size().max(file.size()) > spec::ZIP64_BYTES_THR) @@ -1323,14 +1327,15 @@ impl ZipWriter { /// 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( + pub fn add_symlink( &mut self, name: N, target: T, mut options: FileOptions, ) -> ZipResult<()> where - N: Into>, + N: Into> + ToOwned, + NToOwned: Into>, T: Into>, { if options.permissions.is_none() { @@ -1450,7 +1455,8 @@ impl ZipWriter { self.finish_file()?; let src_index = self.index_by_name(src_name)?; let mut dest_data = self.files[src_index].to_owned(); - dest_data.file_name = dest_name.into(); + dest_data.file_name = dest_name.to_string().into(); + dest_data.file_name_raw = dest_name.to_string().into_bytes().into(); self.insert_file_data(dest_data)?; Ok(()) } @@ -1803,12 +1809,11 @@ fn write_central_directory_header(writer: &mut T, file: &ZipFileData) writer.write_u16_le(version_made_by)?; // version needed to extract writer.write_u16_le(file.version_needed())?; - // general purpose bit flag - let flag = if !file.file_name.is_ascii() { - 1u16 << 11 - } else { - 0 - } | if file.encrypted { 1u16 << 0 } else { 0 }; + // general puprose bit flag + let is_utf8 = std::str::from_utf8(&file.file_name_raw).is_ok(); + let is_ascii = file.file_name_raw.is_ascii(); + let flag = if is_utf8 && !is_ascii { 1u16 << 11 } else { 0 } + | if file.encrypted { 1u16 << 0 } else { 0 }; writer.write_u16_le(flag)?; // compression method #[allow(deprecated)] @@ -1823,7 +1828,7 @@ fn write_central_directory_header(writer: &mut T, file: &ZipFileData) // uncompressed size writer.write_u32_le(file.uncompressed_size.min(spec::ZIP64_BYTES_THR) as u32)?; // file name length - writer.write_u16_le(file.file_name.as_bytes().len() as u16)?; + writer.write_u16_le(file.file_name_raw.len() as u16)?; // extra field length writer.write_u16_le( zip64_extra_field_length @@ -1841,7 +1846,7 @@ fn write_central_directory_header(writer: &mut T, file: &ZipFileData) // relative offset of local header writer.write_u32_le(file.header_start.min(spec::ZIP64_BYTES_THR) as u32)?; // file name - writer.write_all(file.file_name.as_bytes())?; + writer.write_all(&file.file_name_raw)?; // zip64 extra field writer.write_all(&zip64_extra_field[..zip64_extra_field_length as usize])?; // extra field @@ -1906,7 +1911,7 @@ fn update_local_zip64_extra_field( writer: &mut T, file: &ZipFileData, ) -> ZipResult<()> { - let zip64_extra_field = file.header_start + 30 + file.file_name.as_bytes().len() as u64; + let zip64_extra_field = file.header_start + 30 + file.file_name_raw.len() as u64; writer.seek(SeekFrom::Start(zip64_extra_field + 4))?; writer.write_u64_le(file.uncompressed_size)?; writer.write_u64_le(file.compressed_size)?; @@ -2136,6 +2141,64 @@ mod test { const SECOND_FILENAME: &str = "different_name.xyz"; const THIRD_FILENAME: &str = "third_name.xyz"; + #[test] + fn write_non_utf8() { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + let options = FileOptions { + compression_method: CompressionMethod::Stored, + compression_level: None, + last_modified_time: DateTime::default(), + permissions: Some(33188), + large_file: false, + encrypt_with: None, + extended_options: (), + alignment: 1, + #[cfg(feature = "deflate-zopfli")] + zopfli_buffer_size: None, + }; + + // GB18030 + // "中文" = [214, 208, 206, 196] + let filename = unsafe { String::from_utf8_unchecked(vec![214, 208, 206, 196]) }; + writer.start_file(filename, options).unwrap(); + writer.write_all(b"encoding GB18030").unwrap(); + + // SHIFT_JIS + // "日文" = [147, 250, 149, 182] + let filename = unsafe { String::from_utf8_unchecked(vec![147, 250, 149, 182]) }; + writer.start_file(filename, options).unwrap(); + writer.write_all(b"encoding SHIFT_JIS").unwrap(); + + let result = writer.finish().unwrap(); + assert_eq!(result.get_ref().len(), 224); + + let mut v = Vec::new(); + v.extend_from_slice(include_bytes!("../tests/data/non_utf8.zip")); + + // FIXME: Update the actual file once https://github.com/zip-rs/zip2/pull/106 is merged + v[4] = 10; + v[54] = 10; + v[108] = 10; + v[158] = 10; + + assert_eq!(result.get_ref(), &v); + } + + #[test] + fn path_to_string() { + let mut path = std::path::PathBuf::new(); + #[cfg(windows)] + path.push(r"C:\"); + #[cfg(unix)] + path.push("/"); + path.push("windows"); + path.push(".."); + path.push("."); + path.push("system32"); + let path_str = super::path_to_string(&path); + assert_eq!(&*path_str, "system32"); + } + #[test] fn test_shallow_copy() { let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); diff --git a/tests/data/non_utf8.zip b/tests/data/non_utf8.zip new file mode 100644 index 00000000..be628652 Binary files /dev/null and b/tests/data/non_utf8.zip differ