diff options
Diffstat (limited to 'third_party/rust/zip/src/write.rs')
-rw-r--r-- | third_party/rust/zip/src/write.rs | 558 |
1 files changed, 558 insertions, 0 deletions
diff --git a/third_party/rust/zip/src/write.rs b/third_party/rust/zip/src/write.rs new file mode 100644 index 0000000000..a7d172c316 --- /dev/null +++ b/third_party/rust/zip/src/write.rs @@ -0,0 +1,558 @@ +//! Structs for creating a new zip archive + +use compression::CompressionMethod; +use types::{ZipFileData, System, DEFAULT_VERSION}; +use spec; +use crc32; +use result::{ZipResult, ZipError}; +use std::default::Default; +use std::io; +use std::io::prelude::*; +use std::mem; +use time; +use podio::{WritePodExt, LittleEndian}; +use msdos_time::TmMsDosExt; + +#[cfg(feature = "flate2")] +use flate2; +#[cfg(feature = "flate2")] +use flate2::write::DeflateEncoder; + +#[cfg(feature = "bzip2")] +use bzip2; +#[cfg(feature = "bzip2")] +use bzip2::write::BzEncoder; +#[allow(unused_imports)] // Rust <1.23 compat +use std::ascii::AsciiExt; + +enum GenericZipWriter<W: Write + io::Seek> +{ + Closed, + Storer(W), + #[cfg(feature = "flate2")] + Deflater(DeflateEncoder<W>), + #[cfg(feature = "bzip2")] + Bzip2(BzEncoder<W>), +} + +/// Generator for ZIP files. +/// +/// ``` +/// fn doit() -> zip::result::ZipResult<()> +/// { +/// use std::io::Write; +/// +/// // For this example we write to a buffer, but normally you should use a File +/// let mut buf: &mut [u8] = &mut [0u8; 65536]; +/// let mut w = std::io::Cursor::new(buf); +/// let mut zip = zip::ZipWriter::new(w); +/// +/// let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored); +/// try!(zip.start_file("hello_world.txt", options)); +/// try!(zip.write(b"Hello, World!")); +/// +/// // Optionally finish the zip. (this is also done on drop) +/// try!(zip.finish()); +/// +/// Ok(()) +/// } +/// +/// println!("Result: {:?}", doit().unwrap()); +/// ``` +pub struct ZipWriter<W: Write + io::Seek> +{ + inner: GenericZipWriter<W>, + files: Vec<ZipFileData>, + stats: ZipWriterStats, +} + +#[derive(Default)] +struct ZipWriterStats +{ + crc32: u32, + start: u64, + bytes_written: u64, +} + +/// Metadata for a file to be written +#[derive(Copy, Clone)] +pub struct FileOptions { + compression_method: CompressionMethod, + last_modified_time: time::Tm, + permissions: Option<u32>, +} + +impl FileOptions { + #[cfg(feature = "flate2")] + /// Construct a new FileOptions object + pub fn default() -> FileOptions { + FileOptions { + compression_method: CompressionMethod::Deflated, + last_modified_time: time::now(), + permissions: None, + } + } + + #[cfg(not(feature = "flate2"))] + /// Construct a new FileOptions object + pub fn default() -> FileOptions { + FileOptions { + compression_method: CompressionMethod::Stored, + last_modified_time: time::now(), + permissions: None, + } + } + + /// Set the compression method for the new file + /// + /// The default is `CompressionMethod::Deflated`. If the deflate compression feature is + /// disabled, `CompressionMethod::Stored` becomes the default. + /// otherwise. + pub fn compression_method(mut self, method: CompressionMethod) -> FileOptions { + self.compression_method = method; + self + } + + /// Set the last modified time + /// + /// The default is the current timestamp + pub fn last_modified_time(mut self, mod_time: time::Tm) -> FileOptions { + self.last_modified_time = mod_time; + self + } + + /// Set the permissions for the new file. + /// + /// 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 + pub fn unix_permissions(mut self, mode: u32) -> FileOptions { + self.permissions = Some(mode & 0o777); + self + } +} + +impl<W: Write+io::Seek> Write for ZipWriter<W> +{ + fn write(&mut self, buf: &[u8]) -> io::Result<usize> + { + if self.files.len() == 0 { return Err(io::Error::new(io::ErrorKind::Other, "No file has been started")) } + match self.inner.ref_mut() + { + Some(ref mut w) => { + let write_result = w.write(buf); + if let Ok(count) = write_result { + self.stats.update(&buf[0..count]); + } + write_result + + } + None => Err(io::Error::new(io::ErrorKind::BrokenPipe, "ZipWriter was already closed")), + } + } + + fn flush(&mut self) -> io::Result<()> + { + match self.inner.ref_mut() + { + Some(ref mut w) => w.flush(), + None => Err(io::Error::new(io::ErrorKind::BrokenPipe, "ZipWriter was already closed")), + } + } +} + +impl ZipWriterStats +{ + fn update(&mut self, buf: &[u8]) + { + self.crc32 = crc32::update(self.crc32, buf); + self.bytes_written += buf.len() as u64; + } +} + +impl<W: Write+io::Seek> ZipWriter<W> +{ + /// Initializes the ZipWriter. + /// + /// Before writing to this object, the start_file command should be called. + pub fn new(inner: W) -> ZipWriter<W> + { + ZipWriter + { + inner: GenericZipWriter::Storer(inner), + files: Vec::new(), + stats: Default::default(), + } + } + + /// Start a new file for with the requested options. + fn start_entry<S>(&mut self, name: S, options: FileOptions) -> ZipResult<()> + where S: Into<String> + { + try!(self.finish_file()); + + { + let writer = self.inner.get_plain(); + let header_start = try!(writer.seek(io::SeekFrom::Current(0))); + + let permissions = options.permissions.unwrap_or(0o100644); + let file_name = name.into(); + let file_name_raw = file_name.clone().into_bytes(); + let mut file = ZipFileData + { + system: System::Unix, + version_made_by: DEFAULT_VERSION, + encrypted: false, + compression_method: options.compression_method, + last_modified_time: options.last_modified_time, + crc32: 0, + compressed_size: 0, + uncompressed_size: 0, + file_name: file_name, + file_name_raw: file_name_raw, + file_comment: String::new(), + header_start: header_start, + data_start: 0, + external_attributes: permissions << 16, + }; + try!(write_local_file_header(writer, &file)); + + let header_end = try!(writer.seek(io::SeekFrom::Current(0))); + self.stats.start = header_end; + file.data_start = header_end; + + self.stats.bytes_written = 0; + self.stats.crc32 = 0; + + self.files.push(file); + } + + try!(self.inner.switch_to(options.compression_method)); + + Ok(()) + } + + fn finish_file(&mut self) -> ZipResult<()> + { + try!(self.inner.switch_to(CompressionMethod::Stored)); + let writer = self.inner.get_plain(); + + let file = match self.files.last_mut() + { + None => return Ok(()), + Some(f) => f, + }; + file.crc32 = self.stats.crc32; + file.uncompressed_size = self.stats.bytes_written; + + let file_end = try!(writer.seek(io::SeekFrom::Current(0))); + file.compressed_size = file_end - self.stats.start; + + try!(update_local_file_header(writer, file)); + try!(writer.seek(io::SeekFrom::Start(file_end))); + Ok(()) + } + + /// Starts a file. + pub fn start_file<S>(&mut self, name: S, mut options: FileOptions) -> ZipResult<()> + where S: Into<String> + { + if options.permissions.is_none() { + options.permissions = Some(0o644); + } + *options.permissions.as_mut().unwrap() |= 0o100000; + try!(self.start_entry(name, options)); + Ok(()) + } + + /// Add a directory entry. + /// + /// You should not write data to the file afterwards. + pub fn add_directory<S>(&mut self, name: S, mut options: FileOptions) -> ZipResult<()> + where S: Into<String> + { + if options.permissions.is_none() { + options.permissions = Some(0o755); + } + *options.permissions.as_mut().unwrap() |= 0o40000; + options.compression_method = CompressionMethod::Stored; + try!(self.start_entry(name, options)); + Ok(()) + } + + /// Finish the last file and write all other zip-structures + /// + /// This will return the writer, but one should normally not append any data to the end of the file. + /// Note that the zipfile will also be finished on drop. + pub fn finish(&mut self) -> ZipResult<W> + { + try!(self.finalize()); + let inner = mem::replace(&mut self.inner, GenericZipWriter::Closed); + Ok(inner.unwrap()) + } + + fn finalize(&mut self) -> ZipResult<()> + { + try!(self.finish_file()); + + { + let writer = self.inner.get_plain(); + + let central_start = try!(writer.seek(io::SeekFrom::Current(0))); + for file in self.files.iter() + { + try!(write_central_directory_header(writer, file)); + } + let central_size = try!(writer.seek(io::SeekFrom::Current(0))) - central_start; + + let footer = spec::CentralDirectoryEnd + { + disk_number: 0, + disk_with_central_directory: 0, + number_of_files_on_this_disk: self.files.len() as u16, + number_of_files: self.files.len() as u16, + central_directory_size: central_size as u32, + central_directory_offset: central_start as u32, + zip_file_comment: b"zip-rs".to_vec(), + }; + + try!(footer.write(writer)); + } + + Ok(()) + } +} + +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!(&mut io::stderr(), "ZipWriter drop failed: {:?}", e); + } + } + } +} + +impl<W: Write+io::Seek> GenericZipWriter<W> +{ + fn switch_to(&mut self, compression: CompressionMethod) -> ZipResult<()> + { + match self.current_compression() { + Some(method) if method == compression => return Ok(()), + None => try!(Err(io::Error::new(io::ErrorKind::BrokenPipe, "ZipWriter was already closed"))), + _ => {}, + } + + let bare = match mem::replace(self, GenericZipWriter::Closed) + { + GenericZipWriter::Storer(w) => w, + #[cfg(feature = "flate2")] + GenericZipWriter::Deflater(w) => try!(w.finish()), + #[cfg(feature = "bzip2")] + GenericZipWriter::Bzip2(w) => try!(w.finish()), + GenericZipWriter::Closed => try!(Err(io::Error::new(io::ErrorKind::BrokenPipe, "ZipWriter was already closed"))), + }; + + *self = match compression + { + CompressionMethod::Stored => GenericZipWriter::Storer(bare), + #[cfg(feature = "flate2")] + CompressionMethod::Deflated => GenericZipWriter::Deflater(DeflateEncoder::new(bare, flate2::Compression::default())), + #[cfg(feature = "bzip2")] + CompressionMethod::Bzip2 => GenericZipWriter::Bzip2(BzEncoder::new(bare, bzip2::Compression::Default)), + CompressionMethod::Unsupported(..) => return Err(ZipError::UnsupportedArchive("Unsupported compression")), + }; + + Ok(()) + } + + fn ref_mut(&mut self) -> Option<&mut Write> { + match *self { + GenericZipWriter::Storer(ref mut w) => Some(w as &mut Write), + #[cfg(feature = "flate2")] + GenericZipWriter::Deflater(ref mut w) => Some(w as &mut Write), + #[cfg(feature = "bzip2")] + GenericZipWriter::Bzip2(ref mut w) => Some(w as &mut Write), + GenericZipWriter::Closed => None, + } + } + + fn is_closed(&self) -> bool + { + match *self + { + GenericZipWriter::Closed => true, + _ => false, + } + } + + fn get_plain(&mut self) -> &mut W + { + match *self + { + GenericZipWriter::Storer(ref mut w) => w, + _ => panic!("Should have switched to stored beforehand"), + } + } + + fn current_compression(&self) -> Option<CompressionMethod> { + match *self { + GenericZipWriter::Storer(..) => Some(CompressionMethod::Stored), + #[cfg(feature = "flate2")] + GenericZipWriter::Deflater(..) => Some(CompressionMethod::Deflated), + #[cfg(feature = "bzip2")] + GenericZipWriter::Bzip2(..) => Some(CompressionMethod::Bzip2), + GenericZipWriter::Closed => None, + } + } + + fn unwrap(self) -> W + { + match self + { + GenericZipWriter::Storer(w) => w, + _ => panic!("Should have switched to stored beforehand"), + } + } +} + +fn write_local_file_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> +{ + // local file header signature + try!(writer.write_u32::<LittleEndian>(spec::LOCAL_FILE_HEADER_SIGNATURE)); + // version needed to extract + try!(writer.write_u16::<LittleEndian>(file.version_needed())); + // general purpose bit flag + let flag = if !file.file_name.is_ascii() { 1u16 << 11 } else { 0 }; + try!(writer.write_u16::<LittleEndian>(flag)); + // Compression method + try!(writer.write_u16::<LittleEndian>(file.compression_method.to_u16())); + // last mod file time and last mod file date + let msdos_datetime = try!(file.last_modified_time.to_msdos()); + try!(writer.write_u16::<LittleEndian>(msdos_datetime.timepart)); + try!(writer.write_u16::<LittleEndian>(msdos_datetime.datepart)); + // crc-32 + try!(writer.write_u32::<LittleEndian>(file.crc32)); + // compressed size + try!(writer.write_u32::<LittleEndian>(file.compressed_size as u32)); + // uncompressed size + try!(writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)); + // file name length + try!(writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)); + // extra field length + let extra_field = try!(build_extra_field(file)); + try!(writer.write_u16::<LittleEndian>(extra_field.len() as u16)); + // file name + try!(writer.write_all(file.file_name.as_bytes())); + // extra field + try!(writer.write_all(&extra_field)); + + Ok(()) +} + +fn update_local_file_header<T: Write+io::Seek>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> +{ + const CRC32_OFFSET : u64 = 14; + try!(writer.seek(io::SeekFrom::Start(file.header_start + CRC32_OFFSET))); + try!(writer.write_u32::<LittleEndian>(file.crc32)); + try!(writer.write_u32::<LittleEndian>(file.compressed_size as u32)); + try!(writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)); + Ok(()) +} + +fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> +{ + // central file header signature + try!(writer.write_u32::<LittleEndian>(spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE)); + // version made by + let version_made_by = (file.system as u16) << 8 | (file.version_made_by as u16); + try!(writer.write_u16::<LittleEndian>(version_made_by)); + // version needed to extract + try!(writer.write_u16::<LittleEndian>(file.version_needed())); + // general puprose bit flag + let flag = if !file.file_name.is_ascii() { 1u16 << 11 } else { 0 }; + try!(writer.write_u16::<LittleEndian>(flag)); + // compression method + try!(writer.write_u16::<LittleEndian>(file.compression_method.to_u16())); + // last mod file time + date + let msdos_datetime = try!(file.last_modified_time.to_msdos()); + try!(writer.write_u16::<LittleEndian>(msdos_datetime.timepart)); + try!(writer.write_u16::<LittleEndian>(msdos_datetime.datepart)); + // crc-32 + try!(writer.write_u32::<LittleEndian>(file.crc32)); + // compressed size + try!(writer.write_u32::<LittleEndian>(file.compressed_size as u32)); + // uncompressed size + try!(writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)); + // file name length + try!(writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)); + // extra field length + let extra_field = try!(build_extra_field(file)); + try!(writer.write_u16::<LittleEndian>(extra_field.len() as u16)); + // file comment length + try!(writer.write_u16::<LittleEndian>(0)); + // disk number start + try!(writer.write_u16::<LittleEndian>(0)); + // internal file attribytes + try!(writer.write_u16::<LittleEndian>(0)); + // external file attributes + try!(writer.write_u32::<LittleEndian>(file.external_attributes)); + // relative offset of local header + try!(writer.write_u32::<LittleEndian>(file.header_start as u32)); + // file name + try!(writer.write_all(file.file_name.as_bytes())); + // extra field + try!(writer.write_all(&extra_field)); + // file comment + // <none> + + Ok(()) +} + +fn build_extra_field(_file: &ZipFileData) -> ZipResult<Vec<u8>> +{ + let writer = Vec::new(); + // Future work + Ok(writer) +} + +#[cfg(test)] +mod test { + use std::io; + use std::io::Write; + use time; + use super::{FileOptions, ZipWriter}; + use compression::CompressionMethod; + + #[test] + fn write_empty_zip() { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + let result = writer.finish().unwrap(); + assert_eq!(result.get_ref().len(), 28); + let v: Vec<u8> = vec![80, 75, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 122, 105, 112, 45, 114, 115]; + assert_eq!(result.get_ref(), &v); + } + + #[test] + fn write_mimetype_zip() { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + let mut mtime = time::empty_tm(); + mtime.tm_year = 80; + mtime.tm_mday = 1; + let options = FileOptions { + compression_method: CompressionMethod::Stored, + last_modified_time: mtime, + permissions: Some(33188), + }; + writer.start_file("mimetype", options).unwrap(); + writer.write(b"application/vnd.oasis.opendocument.text").unwrap(); + let result = writer.finish().unwrap(); + assert_eq!(result.get_ref().len(), 159); + let mut v = Vec::new(); + v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip")); + assert_eq!(result.get_ref(), &v); + } +} |