diff options
Diffstat (limited to 'third_party/rust/zip/src/write.rs')
-rw-r--r-- | third_party/rust/zip/src/write.rs | 1471 |
1 files changed, 1471 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..61ce378c0c --- /dev/null +++ b/third_party/rust/zip/src/write.rs @@ -0,0 +1,1471 @@ +//! Types for creating ZIP archives + +use crate::compression::CompressionMethod; +use crate::read::{central_header_to_zip_file, ZipArchive, ZipFile}; +use crate::result::{ZipError, ZipResult}; +use crate::spec; +use crate::types::{AtomicU64, DateTime, System, ZipFileData, DEFAULT_VERSION}; +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use crc32fast::Hasher; +use std::default::Default; +use std::io; +use std::io::prelude::*; +use std::mem; + +#[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" +))] +use flate2::write::DeflateEncoder; + +#[cfg(feature = "bzip2")] +use bzip2::write::BzEncoder; + +#[cfg(feature = "time")] +use time::OffsetDateTime; + +#[cfg(feature = "zstd")] +use zstd::stream::write::Encoder as ZstdEncoder; + +enum GenericZipWriter<W: Write + io::Seek> { + Closed, + Storer(W), + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + Deflater(DeflateEncoder<W>), + #[cfg(feature = "bzip2")] + Bzip2(BzEncoder<W>), + #[cfg(feature = "zstd")] + Zstd(ZstdEncoder<'static, W>), +} +// Put the struct declaration in a private module to convince rustdoc to display ZipWriter nicely +pub(crate) mod zip_writer { + use super::*; + /// ZIP archive generator + /// + /// Handles the bookkeeping involved in building an archive, and provides an + /// API to edit its contents. + /// + /// ``` + /// # fn doit() -> zip::result::ZipResult<()> + /// # { + /// # use zip::ZipWriter; + /// use std::io::Write; + /// use zip::write::FileOptions; + /// + /// // We use a buffer here, though you'd normally use a `File` + /// let mut buf = [0; 65536]; + /// let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf[..])); + /// + /// let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored); + /// zip.start_file("hello_world.txt", options)?; + /// zip.write(b"Hello, World!")?; + /// + /// // Apply the changes you've made. + /// // Dropping the `ZipWriter` will have the same effect, but may silently fail + /// zip.finish()?; + /// + /// # Ok(()) + /// # } + /// # doit().unwrap(); + /// ``` + pub struct ZipWriter<W: Write + io::Seek> { + pub(super) inner: GenericZipWriter<W>, + pub(super) files: Vec<ZipFileData>, + pub(super) stats: ZipWriterStats, + pub(super) writing_to_file: bool, + pub(super) writing_to_extra_field: bool, + pub(super) writing_to_central_extra_field_only: bool, + pub(super) writing_raw: bool, + pub(super) comment: Vec<u8>, + } +} +pub use zip_writer::ZipWriter; + +#[derive(Default)] +struct ZipWriterStats { + hasher: Hasher, + start: u64, + bytes_written: u64, +} + +struct ZipRawValues { + crc32: u32, + compressed_size: u64, + uncompressed_size: u64, +} + +/// Metadata for a file to be written +#[derive(Copy, Clone)] +pub struct FileOptions { + compression_method: CompressionMethod, + compression_level: Option<i32>, + last_modified_time: DateTime, + permissions: Option<u32>, + large_file: bool, +} + +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 + /// disabled, `CompressionMethod::Stored` becomes the default. + #[must_use] + pub fn compression_method(mut self, method: CompressionMethod) -> FileOptions { + self.compression_method = method; + self + } + + /// Set the compression level for the new file + /// + /// `None` value specifies default compression level. + /// + /// Range of values depends on compression method: + /// * `Deflated`: 0 - 9. Default is 6 + /// * `Bzip2`: 0 - 9. Default is 6 + /// * `Zstd`: -7 - 22, with zero being mapped to default level. Default is 3 + /// * others: only `None` is allowed + #[must_use] + pub fn compression_level(mut self, level: Option<i32>) -> FileOptions { + self.compression_level = level; + self + } + + /// Set the last modified time + /// + /// The default is the current timestamp if the 'time' feature is enabled, and 1980-01-01 + /// otherwise + #[must_use] + pub fn last_modified_time(mut self, mod_time: DateTime) -> 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. + /// + /// 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); + self + } + + /// Set whether the new file's compressed and uncompressed size is less than 4 GiB. + /// + /// If set to `false` and the file exceeds the limit, an I/O error is thrown. If set to `true`, + /// readers will require ZIP64 support and if the file does not exceed the limit, 20 B are + /// wasted. The default is `false`. + #[must_use] + pub fn large_file(mut self, large: bool) -> FileOptions { + self.large_file = large; + self + } +} + +impl Default for FileOptions { + fn default() -> Self { + Self::default() + } +} + +impl<W: Write + io::Seek> Write for ZipWriter<W> { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + if !self.writing_to_file { + return Err(io::Error::new( + io::ErrorKind::Other, + "No file has been started", + )); + } + match self.inner.ref_mut() { + Some(ref mut w) => { + if self.writing_to_extra_field { + self.files.last_mut().unwrap().extra_field.write(buf) + } else { + let write_result = w.write(buf); + if let Ok(count) = write_result { + self.stats.update(&buf[0..count]); + if self.stats.bytes_written > spec::ZIP64_BYTES_THR + && !self.files.last_mut().unwrap().large_file + { + let _inner = mem::replace(&mut self.inner, GenericZipWriter::Closed); + return Err(io::Error::new( + io::ErrorKind::Other, + "Large file option has not been set", + )); + } + } + 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.hasher.update(buf); + self.bytes_written += buf.len() as u64; + } +} + +impl<A: Read + Write + io::Seek> ZipWriter<A> { + /// Initializes the archive from an existing ZIP archive, making it ready for append. + pub fn new_append(mut readwriter: A) -> ZipResult<ZipWriter<A>> { + let (footer, cde_start_pos) = spec::CentralDirectoryEnd::find_and_parse(&mut readwriter)?; + + if footer.disk_number != footer.disk_with_central_directory { + return Err(ZipError::UnsupportedArchive( + "Support for multi-disk files is not implemented", + )); + } + + let (archive_offset, directory_start, number_of_files) = + ZipArchive::get_directory_counts(&mut readwriter, &footer, cde_start_pos)?; + + if readwriter + .seek(io::SeekFrom::Start(directory_start)) + .is_err() + { + return Err(ZipError::InvalidArchive( + "Could not seek to start of central directory", + )); + } + + let files = (0..number_of_files) + .map(|_| central_header_to_zip_file(&mut readwriter, archive_offset)) + .collect::<Result<Vec<_>, _>>()?; + + let _ = readwriter.seek(io::SeekFrom::Start(directory_start)); // seek directory_start to overwrite it + + Ok(ZipWriter { + inner: GenericZipWriter::Storer(readwriter), + files, + stats: Default::default(), + writing_to_file: false, + writing_to_extra_field: false, + writing_to_central_extra_field_only: false, + comment: footer.zip_file_comment, + writing_raw: true, // avoid recomputing the last file's header + }) + } +} + +impl<W: Write + io::Seek> ZipWriter<W> { + /// Initializes the archive. + /// + /// Before writing to this object, the [`ZipWriter::start_file`] function should be called. + pub fn new(inner: W) -> ZipWriter<W> { + ZipWriter { + inner: GenericZipWriter::Storer(inner), + files: Vec::new(), + stats: Default::default(), + writing_to_file: false, + writing_to_extra_field: false, + writing_to_central_extra_field_only: false, + writing_raw: false, + comment: Vec::new(), + } + } + + /// Set ZIP archive comment. + pub fn set_comment<S>(&mut self, comment: S) + where + S: Into<String>, + { + self.set_raw_comment(comment.into().into()) + } + + /// Set ZIP archive comment. + /// + /// This sets the raw bytes of the comment. The comment + /// is typically expected to be encoded in UTF-8 + pub fn set_raw_comment(&mut self, comment: Vec<u8>) { + self.comment = comment; + } + + /// Start a new file for with the requested options. + fn start_entry<S>( + &mut self, + name: S, + options: FileOptions, + raw_values: Option<ZipRawValues>, + ) -> ZipResult<()> + where + S: Into<String>, + { + self.finish_file()?; + + let raw_values = raw_values.unwrap_or(ZipRawValues { + crc32: 0, + compressed_size: 0, + uncompressed_size: 0, + }); + + { + let writer = self.inner.get_plain(); + let header_start = writer.stream_position()?; + + let permissions = options.permissions.unwrap_or(0o100644); + let mut file = ZipFileData { + system: System::Unix, + version_made_by: DEFAULT_VERSION, + encrypted: false, + using_data_descriptor: false, + compression_method: options.compression_method, + compression_level: options.compression_level, + last_modified_time: options.last_modified_time, + crc32: raw_values.crc32, + compressed_size: raw_values.compressed_size, + uncompressed_size: raw_values.uncompressed_size, + file_name: name.into(), + file_name_raw: Vec::new(), // Never used for saving + extra_field: Vec::new(), + file_comment: String::new(), + header_start, + data_start: AtomicU64::new(0), + central_header_start: 0, + external_attributes: permissions << 16, + large_file: options.large_file, + aes_mode: None, + }; + write_local_file_header(writer, &file)?; + + let header_end = writer.stream_position()?; + self.stats.start = header_end; + *file.data_start.get_mut() = header_end; + + self.stats.bytes_written = 0; + self.stats.hasher = Hasher::new(); + + self.files.push(file); + } + + Ok(()) + } + + fn finish_file(&mut self) -> ZipResult<()> { + if self.writing_to_extra_field { + // Implicitly calling [`ZipWriter::end_extra_data`] for empty files. + self.end_extra_data()?; + } + self.inner.switch_to(CompressionMethod::Stored, None)?; + let writer = self.inner.get_plain(); + + if !self.writing_raw { + let file = match self.files.last_mut() { + None => return Ok(()), + Some(f) => f, + }; + file.crc32 = self.stats.hasher.clone().finalize(); + file.uncompressed_size = self.stats.bytes_written; + + let file_end = writer.stream_position()?; + file.compressed_size = file_end - self.stats.start; + + update_local_file_header(writer, file)?; + writer.seek(io::SeekFrom::Start(file_end))?; + } + + self.writing_to_file = false; + self.writing_raw = false; + Ok(()) + } + + /// Create a file in the archive and start writing its' contents. + /// + /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`] + 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; + self.start_entry(name, options, None)?; + self.inner + .switch_to(options.compression_method, options.compression_level)?; + self.writing_to_file = true; + Ok(()) + } + + /// Starts a file, taking a Path as argument. + /// + /// 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", + note = "by stripping `..`s from the path, the meaning of paths can change. Use `start_file` instead." + )] + pub fn start_file_from_path( + &mut self, + path: &std::path::Path, + options: FileOptions, + ) -> ZipResult<()> { + self.start_file(path_to_string(path), options) + } + + /// Create an aligned file in the archive and start writing its' contents. + /// + /// Returns the number of padding bytes required to align the file. + /// + /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`] + pub fn start_file_aligned<S>( + &mut self, + name: S, + options: FileOptions, + align: u16, + ) -> Result<u64, ZipError> + where + S: Into<String>, + { + let data_start = self.start_file_with_extra_data(name, options)?; + let align = align as u64; + if align > 1 && data_start % align != 0 { + let pad_length = (align - (data_start + 4) % align) % align; + let pad = vec![0; pad_length as usize]; + self.write_all(b"za").map_err(ZipError::from)?; // 0x617a + self.write_u16::<LittleEndian>(pad.len() as u16) + .map_err(ZipError::from)?; + self.write_all(&pad).map_err(ZipError::from)?; + assert_eq!(self.end_local_start_central_extra_data()? % align, 0); + } + let extra_data_end = self.end_extra_data()?; + Ok(extra_data_end - data_start) + } + + /// Create a file in the archive and start writing its extra data first. + /// + /// Finish writing extra data and start writing file data with [`ZipWriter::end_extra_data`]. + /// Optionally, distinguish local from central extra data with + /// [`ZipWriter::end_local_start_central_extra_data`]. + /// + /// Returns the preliminary starting offset of the file data without any extra data allowing to + /// align the file data by calculating a pad length to be prepended as part of the extra data. + /// + /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`] + /// + /// ``` + /// use byteorder::{LittleEndian, WriteBytesExt}; + /// use zip::{ZipArchive, ZipWriter, result::ZipResult}; + /// use zip::{write::FileOptions, CompressionMethod}; + /// use std::io::{Write, Cursor}; + /// + /// # fn main() -> ZipResult<()> { + /// let mut archive = Cursor::new(Vec::new()); + /// + /// { + /// let mut zip = ZipWriter::new(&mut archive); + /// let options = FileOptions::default() + /// .compression_method(CompressionMethod::Stored); + /// + /// zip.start_file_with_extra_data("identical_extra_data.txt", options)?; + /// let extra_data = b"local and central extra data"; + /// zip.write_u16::<LittleEndian>(0xbeef)?; + /// zip.write_u16::<LittleEndian>(extra_data.len() as u16)?; + /// zip.write_all(extra_data)?; + /// zip.end_extra_data()?; + /// zip.write_all(b"file data")?; + /// + /// let data_start = zip.start_file_with_extra_data("different_extra_data.txt", options)?; + /// let extra_data = b"local extra data"; + /// zip.write_u16::<LittleEndian>(0xbeef)?; + /// zip.write_u16::<LittleEndian>(extra_data.len() as u16)?; + /// zip.write_all(extra_data)?; + /// let data_start = data_start as usize + 4 + extra_data.len() + 4; + /// let align = 64; + /// let pad_length = (align - data_start % align) % align; + /// assert_eq!(pad_length, 19); + /// zip.write_u16::<LittleEndian>(0xdead)?; + /// zip.write_u16::<LittleEndian>(pad_length as u16)?; + /// zip.write_all(&vec![0; pad_length])?; + /// let data_start = zip.end_local_start_central_extra_data()?; + /// assert_eq!(data_start as usize % align, 0); + /// let extra_data = b"central extra data"; + /// zip.write_u16::<LittleEndian>(0xbeef)?; + /// zip.write_u16::<LittleEndian>(extra_data.len() as u16)?; + /// zip.write_all(extra_data)?; + /// zip.end_extra_data()?; + /// zip.write_all(b"file data")?; + /// + /// zip.finish()?; + /// } + /// + /// let mut zip = ZipArchive::new(archive)?; + /// assert_eq!(&zip.by_index(0)?.extra_data()[4..], b"local and central extra data"); + /// assert_eq!(&zip.by_index(1)?.extra_data()[4..], b"central extra data"); + /// # Ok(()) + /// # } + /// ``` + pub fn start_file_with_extra_data<S>( + &mut self, + name: S, + mut options: FileOptions, + ) -> ZipResult<u64> + where + S: Into<String>, + { + if options.permissions.is_none() { + options.permissions = Some(0o644); + } + *options.permissions.as_mut().unwrap() |= 0o100000; + self.start_entry(name, options, None)?; + self.writing_to_file = true; + self.writing_to_extra_field = true; + Ok(self.files.last().unwrap().data_start.load()) + } + + /// End local and start central extra data. Requires [`ZipWriter::start_file_with_extra_data`]. + /// + /// Returns the final starting offset of the file data. + pub fn end_local_start_central_extra_data(&mut self) -> ZipResult<u64> { + let data_start = self.end_extra_data()?; + self.files.last_mut().unwrap().extra_field.clear(); + self.writing_to_extra_field = true; + self.writing_to_central_extra_field_only = true; + Ok(data_start) + } + + /// End extra data and start file data. Requires [`ZipWriter::start_file_with_extra_data`]. + /// + /// Returns the final starting offset of the file data. + pub fn end_extra_data(&mut self) -> ZipResult<u64> { + // Require `start_file_with_extra_data()`. Ensures `file` is some. + if !self.writing_to_extra_field { + return Err(ZipError::Io(io::Error::new( + io::ErrorKind::Other, + "Not writing to extra field", + ))); + } + let file = self.files.last_mut().unwrap(); + + validate_extra_data(file)?; + + let data_start = file.data_start.get_mut(); + + if !self.writing_to_central_extra_field_only { + let writer = self.inner.get_plain(); + + // Append extra data to local file header and keep it for central file header. + writer.write_all(&file.extra_field)?; + + // Update final `data_start`. + let header_end = *data_start + file.extra_field.len() as u64; + self.stats.start = header_end; + *data_start = header_end; + + // Update extra field length in local file header. + let extra_field_length = + if file.large_file { 20 } else { 0 } + file.extra_field.len() as u16; + writer.seek(io::SeekFrom::Start(file.header_start + 28))?; + writer.write_u16::<LittleEndian>(extra_field_length)?; + writer.seek(io::SeekFrom::Start(header_end))?; + + self.inner + .switch_to(file.compression_method, file.compression_level)?; + } + + self.writing_to_extra_field = false; + self.writing_to_central_extra_field_only = false; + Ok(*data_start) + } + + /// Add a new file using the already compressed data from a ZIP file being read and renames it, this + /// allows faster copies of the `ZipFile` since there is no need to decompress and compress it again. + /// Any `ZipFile` metadata is copied and not checked, for example the file CRC. + + /// ```no_run + /// use std::fs::File; + /// use std::io::{Read, Seek, Write}; + /// use zip::{ZipArchive, ZipWriter}; + /// + /// fn copy_rename<R, W>( + /// src: &mut ZipArchive<R>, + /// dst: &mut ZipWriter<W>, + /// ) -> zip::result::ZipResult<()> + /// where + /// R: Read + Seek, + /// W: Write + Seek, + /// { + /// // Retrieve file entry by name + /// let file = src.by_name("src_file.txt")?; + /// + /// // Copy and rename the previously obtained file entry to the destination zip archive + /// dst.raw_copy_file_rename(file, "new_name.txt")?; + /// + /// Ok(()) + /// } + /// ``` + pub fn raw_copy_file_rename<S>(&mut self, mut file: ZipFile, name: S) -> ZipResult<()> + where + S: Into<String>, + { + let mut options = FileOptions::default() + .large_file(file.compressed_size().max(file.size()) > spec::ZIP64_BYTES_THR) + .last_modified_time(file.last_modified()) + .compression_method(file.compression()); + if let Some(perms) = file.unix_mode() { + options = options.unix_permissions(perms); + } + + let raw_values = ZipRawValues { + crc32: file.crc32(), + compressed_size: file.compressed_size(), + uncompressed_size: file.size(), + }; + + self.start_entry(name, options, Some(raw_values))?; + self.writing_to_file = true; + self.writing_raw = true; + + io::copy(file.get_raw_reader(), self)?; + + Ok(()) + } + + /// Add a new file using the already compressed data from a ZIP file being read, this allows faster + /// copies of the `ZipFile` since there is no need to decompress and compress it again. Any `ZipFile` + /// metadata is copied and not checked, for example the file CRC. + /// + /// ```no_run + /// use std::fs::File; + /// use std::io::{Read, Seek, Write}; + /// use zip::{ZipArchive, ZipWriter}; + /// + /// fn copy<R, W>(src: &mut ZipArchive<R>, dst: &mut ZipWriter<W>) -> zip::result::ZipResult<()> + /// where + /// R: Read + Seek, + /// W: Write + Seek, + /// { + /// // Retrieve file entry by name + /// let file = src.by_name("src_file.txt")?; + /// + /// // Copy the previously obtained file entry to the destination zip archive + /// dst.raw_copy_file(file)?; + /// + /// Ok(()) + /// } + /// ``` + pub fn raw_copy_file(&mut self, file: ZipFile) -> ZipResult<()> { + let name = file.name().to_owned(); + self.raw_copy_file_rename(file, name) + } + + /// Add a directory entry. + /// + /// You can't 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; + + let name_as_string = name.into(); + // Append a slash to the filename if it does not end with it. + let name_with_slash = match name_as_string.chars().last() { + Some('/') | Some('\\') => name_as_string, + _ => name_as_string + "/", + }; + + self.start_entry(name_with_slash, options, None)?; + self.writing_to_file = false; + Ok(()) + } + + /// Add a directory entry, taking a Path as argument. + /// + /// 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", + note = "by stripping `..`s from the path, the meaning of paths can change. Use `add_directory` instead." + )] + pub fn add_directory_from_path( + &mut self, + path: &std::path::Path, + options: FileOptions, + ) -> ZipResult<()> { + self.add_directory(path_to_string(path), options) + } + + /// 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> { + self.finalize()?; + let inner = mem::replace(&mut self.inner, GenericZipWriter::Closed); + 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.stream_position()?; + for file in self.files.iter() { + write_central_directory_header(writer, file)?; + } + 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 + { + let zip64_footer = spec::Zip64CentralDirectoryEnd { + version_made_by: DEFAULT_VERSION as u16, + version_needed_to_extract: DEFAULT_VERSION as u16, + disk_number: 0, + disk_with_central_directory: 0, + number_of_files_on_this_disk: self.files.len() as u64, + number_of_files: self.files.len() as u64, + central_directory_size: central_size, + central_directory_offset: central_start, + }; + + zip64_footer.write(writer)?; + + let zip64_footer = spec::Zip64CentralDirectoryEndLocator { + disk_with_central_directory: 0, + end_of_central_directory_offset: central_start + central_size, + number_of_disks: 1, + }; + + zip64_footer.write(writer)?; + } + + let number_of_files = self.files.len().min(spec::ZIP64_ENTRY_THR) as u16; + let footer = spec::CentralDirectoryEnd { + disk_number: 0, + disk_with_central_directory: 0, + zip_file_comment: self.comment.clone(), + number_of_files_on_this_disk: number_of_files, + number_of_files, + central_directory_size: central_size.min(spec::ZIP64_BYTES_THR) as u32, + central_directory_offset: central_start.min(spec::ZIP64_BYTES_THR) as u32, + }; + + 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!(io::stderr(), "ZipWriter drop failed: {:?}", e); + } + } + } +} + +impl<W: Write + io::Seek> GenericZipWriter<W> { + fn switch_to( + &mut self, + compression: CompressionMethod, + compression_level: Option<i32>, + ) -> ZipResult<()> { + match self.current_compression() { + Some(method) if method == compression => return Ok(()), + None => { + return Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "ZipWriter was already closed", + ) + .into()) + } + _ => {} + } + + let bare = match mem::replace(self, GenericZipWriter::Closed) { + GenericZipWriter::Storer(w) => w, + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + GenericZipWriter::Deflater(w) => w.finish()?, + #[cfg(feature = "bzip2")] + GenericZipWriter::Bzip2(w) => w.finish()?, + #[cfg(feature = "zstd")] + GenericZipWriter::Zstd(w) => w.finish()?, + GenericZipWriter::Closed => { + return Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "ZipWriter was already closed", + ) + .into()) + } + }; + + *self = { + #[allow(deprecated)] + match compression { + CompressionMethod::Stored => { + if compression_level.is_some() { + return Err(ZipError::UnsupportedArchive( + "Unsupported compression level", + )); + } + + GenericZipWriter::Storer(bare) + } + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + CompressionMethod::Deflated => GenericZipWriter::Deflater(DeflateEncoder::new( + bare, + flate2::Compression::new( + clamp_opt( + compression_level + .unwrap_or(flate2::Compression::default().level() as i32), + deflate_compression_level_range(), + ) + .ok_or(ZipError::UnsupportedArchive( + "Unsupported compression level", + ))? as u32, + ), + )), + #[cfg(feature = "bzip2")] + CompressionMethod::Bzip2 => GenericZipWriter::Bzip2(BzEncoder::new( + bare, + bzip2::Compression::new( + clamp_opt( + compression_level + .unwrap_or(bzip2::Compression::default().level() as i32), + bzip2_compression_level_range(), + ) + .ok_or(ZipError::UnsupportedArchive( + "Unsupported compression level", + ))? as u32, + ), + )), + CompressionMethod::AES => { + return Err(ZipError::UnsupportedArchive( + "AES compression is not supported for writing", + )) + } + #[cfg(feature = "zstd")] + CompressionMethod::Zstd => GenericZipWriter::Zstd( + ZstdEncoder::new( + bare, + clamp_opt( + compression_level.unwrap_or(zstd::DEFAULT_COMPRESSION_LEVEL), + zstd::compression_level_range(), + ) + .ok_or(ZipError::UnsupportedArchive( + "Unsupported compression level", + ))?, + ) + .unwrap(), + ), + CompressionMethod::Unsupported(..) => { + return Err(ZipError::UnsupportedArchive("Unsupported compression")) + } + } + }; + + Ok(()) + } + + fn ref_mut(&mut self) -> Option<&mut dyn Write> { + match *self { + GenericZipWriter::Storer(ref mut w) => Some(w as &mut dyn Write), + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + GenericZipWriter::Deflater(ref mut w) => Some(w as &mut dyn Write), + #[cfg(feature = "bzip2")] + GenericZipWriter::Bzip2(ref mut w) => Some(w as &mut dyn Write), + #[cfg(feature = "zstd")] + GenericZipWriter::Zstd(ref mut w) => Some(w as &mut dyn Write), + GenericZipWriter::Closed => None, + } + } + + fn is_closed(&self) -> bool { + matches!(*self, GenericZipWriter::Closed) + } + + 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(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + GenericZipWriter::Deflater(..) => Some(CompressionMethod::Deflated), + #[cfg(feature = "bzip2")] + GenericZipWriter::Bzip2(..) => Some(CompressionMethod::Bzip2), + #[cfg(feature = "zstd")] + GenericZipWriter::Zstd(..) => Some(CompressionMethod::Zstd), + GenericZipWriter::Closed => None, + } + } + + fn unwrap(self) -> W { + match self { + GenericZipWriter::Storer(w) => w, + _ => panic!("Should have switched to stored beforehand"), + } + } +} + +#[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" +))] +fn deflate_compression_level_range() -> std::ops::RangeInclusive<i32> { + let min = flate2::Compression::none().level() as i32; + let max = flate2::Compression::best().level() as i32; + min..=max +} + +#[cfg(feature = "bzip2")] +fn bzip2_compression_level_range() -> std::ops::RangeInclusive<i32> { + let min = bzip2::Compression::none().level() as i32; + let max = bzip2::Compression::best().level() as i32; + min..=max +} + +#[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib", + feature = "bzip2", + feature = "zstd" +))] +fn clamp_opt<T: Ord + Copy>(value: T, range: std::ops::RangeInclusive<T>) -> Option<T> { + if range.contains(&value) { + Some(value) + } else { + None + } +} + +fn write_local_file_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> { + // local file header signature + writer.write_u32::<LittleEndian>(spec::LOCAL_FILE_HEADER_SIGNATURE)?; + // version needed to extract + writer.write_u16::<LittleEndian>(file.version_needed())?; + // general purpose bit flag + let flag = if !file.file_name.is_ascii() { + 1u16 << 11 + } else { + 0 + }; + writer.write_u16::<LittleEndian>(flag)?; + // Compression method + #[allow(deprecated)] + writer.write_u16::<LittleEndian>(file.compression_method.to_u16())?; + // last mod file time and last mod file date + writer.write_u16::<LittleEndian>(file.last_modified_time.timepart())?; + writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?; + // crc-32 + writer.write_u32::<LittleEndian>(file.crc32)?; + // compressed size and uncompressed size + if file.large_file { + writer.write_u32::<LittleEndian>(spec::ZIP64_BYTES_THR as u32)?; + writer.write_u32::<LittleEndian>(spec::ZIP64_BYTES_THR as u32)?; + } else { + writer.write_u32::<LittleEndian>(file.compressed_size as u32)?; + writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?; + } + // file name length + writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?; + // extra field length + let extra_field_length = if file.large_file { 20 } else { 0 } + file.extra_field.len() as u16; + writer.write_u16::<LittleEndian>(extra_field_length)?; + // file name + writer.write_all(file.file_name.as_bytes())?; + // zip64 extra field + if file.large_file { + write_local_zip64_extra_field(writer, file)?; + } + + Ok(()) +} + +fn update_local_file_header<T: Write + io::Seek>( + writer: &mut T, + file: &ZipFileData, +) -> ZipResult<()> { + const CRC32_OFFSET: u64 = 14; + writer.seek(io::SeekFrom::Start(file.header_start + CRC32_OFFSET))?; + writer.write_u32::<LittleEndian>(file.crc32)?; + if file.large_file { + update_local_zip64_extra_field(writer, file)?; + } else { + // check compressed size as well as it can also be slightly larger than uncompressed size + if file.compressed_size > spec::ZIP64_BYTES_THR { + return Err(ZipError::Io(io::Error::new( + io::ErrorKind::Other, + "Large file option has not been set", + ))); + } + writer.write_u32::<LittleEndian>(file.compressed_size as u32)?; + // uncompressed size is already checked on write to catch it as soon as possible + writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?; + } + Ok(()) +} + +fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> { + // buffer zip64 extra field to determine its variable length + let mut zip64_extra_field = [0; 28]; + let zip64_extra_field_length = + write_central_zip64_extra_field(&mut zip64_extra_field.as_mut(), file)?; + + // central file header signature + 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); + writer.write_u16::<LittleEndian>(version_made_by)?; + // version needed to extract + writer.write_u16::<LittleEndian>(file.version_needed())?; + // general puprose bit flag + let flag = if !file.file_name.is_ascii() { + 1u16 << 11 + } else { + 0 + }; + writer.write_u16::<LittleEndian>(flag)?; + // compression method + #[allow(deprecated)] + writer.write_u16::<LittleEndian>(file.compression_method.to_u16())?; + // last mod file time + date + writer.write_u16::<LittleEndian>(file.last_modified_time.timepart())?; + writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?; + // crc-32 + writer.write_u32::<LittleEndian>(file.crc32)?; + // compressed size + writer.write_u32::<LittleEndian>(file.compressed_size.min(spec::ZIP64_BYTES_THR) as u32)?; + // uncompressed size + writer.write_u32::<LittleEndian>(file.uncompressed_size.min(spec::ZIP64_BYTES_THR) as u32)?; + // file name length + writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?; + // extra field length + writer.write_u16::<LittleEndian>(zip64_extra_field_length + file.extra_field.len() as u16)?; + // file comment length + writer.write_u16::<LittleEndian>(0)?; + // disk number start + writer.write_u16::<LittleEndian>(0)?; + // internal file attribytes + writer.write_u16::<LittleEndian>(0)?; + // external file attributes + writer.write_u32::<LittleEndian>(file.external_attributes)?; + // relative offset of local header + writer.write_u32::<LittleEndian>(file.header_start.min(spec::ZIP64_BYTES_THR) as u32)?; + // file name + writer.write_all(file.file_name.as_bytes())?; + // zip64 extra field + writer.write_all(&zip64_extra_field[..zip64_extra_field_length as usize])?; + // extra field + writer.write_all(&file.extra_field)?; + // file comment + // <none> + + Ok(()) +} + +fn validate_extra_data(file: &ZipFileData) -> ZipResult<()> { + let mut data = file.extra_field.as_slice(); + + if data.len() > spec::ZIP64_ENTRY_THR { + return Err(ZipError::Io(io::Error::new( + io::ErrorKind::InvalidData, + "Extra data exceeds extra field", + ))); + } + + while !data.is_empty() { + let left = data.len(); + if left < 4 { + return Err(ZipError::Io(io::Error::new( + io::ErrorKind::Other, + "Incomplete extra data header", + ))); + } + let kind = data.read_u16::<LittleEndian>()?; + let size = data.read_u16::<LittleEndian>()? as usize; + let left = left - 4; + + if kind == 0x0001 { + return Err(ZipError::Io(io::Error::new( + io::ErrorKind::Other, + "No custom ZIP64 extra data allowed", + ))); + } + + #[cfg(not(feature = "unreserved"))] + { + if kind <= 31 || EXTRA_FIELD_MAPPING.iter().any(|&mapped| mapped == kind) { + return Err(ZipError::Io(io::Error::new( + io::ErrorKind::Other, + format!( + "Extra data header ID {:#06} requires crate feature \"unreserved\"", + kind, + ), + ))); + } + } + + if size > left { + return Err(ZipError::Io(io::Error::new( + io::ErrorKind::Other, + "Extra data size exceeds extra field", + ))); + } + + data = &data[size..]; + } + + Ok(()) +} + +fn write_local_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> { + // This entry in the Local header MUST include BOTH original + // and compressed file size fields. + writer.write_u16::<LittleEndian>(0x0001)?; + writer.write_u16::<LittleEndian>(16)?; + writer.write_u64::<LittleEndian>(file.uncompressed_size)?; + writer.write_u64::<LittleEndian>(file.compressed_size)?; + // Excluded fields: + // u32: disk start number + Ok(()) +} + +fn update_local_zip64_extra_field<T: Write + io::Seek>( + writer: &mut T, + file: &ZipFileData, +) -> ZipResult<()> { + let zip64_extra_field = file.header_start + 30 + file.file_name.as_bytes().len() as u64; + writer.seek(io::SeekFrom::Start(zip64_extra_field + 4))?; + writer.write_u64::<LittleEndian>(file.uncompressed_size)?; + writer.write_u64::<LittleEndian>(file.compressed_size)?; + // Excluded fields: + // u32: disk start number + Ok(()) +} + +fn write_central_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<u16> { + // The order of the fields in the zip64 extended + // information record is fixed, but the fields MUST + // only appear if the corresponding Local or Central + // directory record field is set to 0xFFFF or 0xFFFFFFFF. + let mut size = 0; + let uncompressed_size = file.uncompressed_size > spec::ZIP64_BYTES_THR; + let compressed_size = file.compressed_size > spec::ZIP64_BYTES_THR; + let header_start = file.header_start > spec::ZIP64_BYTES_THR; + if uncompressed_size { + size += 8; + } + if compressed_size { + size += 8; + } + if header_start { + size += 8; + } + if size > 0 { + writer.write_u16::<LittleEndian>(0x0001)?; + writer.write_u16::<LittleEndian>(size)?; + size += 4; + + if uncompressed_size { + writer.write_u64::<LittleEndian>(file.uncompressed_size)?; + } + if compressed_size { + writer.write_u64::<LittleEndian>(file.compressed_size)?; + } + if header_start { + writer.write_u64::<LittleEndian>(file.header_start)?; + } + // Excluded fields: + // u32: disk start number + } + Ok(size) +} + +fn path_to_string(path: &std::path::Path) -> String { + let mut path_str = String::new(); + for component in path.components() { + if let std::path::Component::Normal(os_str) = component { + if !path_str.is_empty() { + path_str.push('/'); + } + path_str.push_str(&*os_str.to_string_lossy()); + } + } + path_str +} + +#[cfg(test)] +mod test { + use super::{FileOptions, ZipWriter}; + use crate::compression::CompressionMethod; + use crate::types::DateTime; + use std::io; + use std::io::Write; + + #[test] + fn write_empty_zip() { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + writer.set_comment("ZIP"); + let result = writer.finish().unwrap(); + assert_eq!(result.get_ref().len(), 25); + assert_eq!( + *result.get_ref(), + [80, 75, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 90, 73, 80] + ); + } + + #[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 + .add_directory( + "test", + 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 directory is not allowed, and will not write any data") + .is_err()); + let result = writer.finish().unwrap(); + assert_eq!(result.get_ref().len(), 108); + assert_eq!( + *result.get_ref(), + &[ + 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 5, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 1, 2, 46, 3, 20, 0, 0, 0, 0, 0, + 163, 165, 15, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 237, 65, 0, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, + 1, 0, 51, 0, 0, 0, 35, 0, 0, 0, 0, 0, + ] as &[u8] + ); + } + + #[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 { + compression_method: CompressionMethod::Stored, + compression_level: None, + last_modified_time: DateTime::default(), + permissions: Some(33188), + large_file: false, + }; + writer.start_file("mimetype", options).unwrap(); + writer + .write_all(b"application/vnd.oasis.opendocument.text") + .unwrap(); + let result = writer.finish().unwrap(); + + assert_eq!(result.get_ref().len(), 153); + let mut v = Vec::new(); + v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip")); + 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, "windows/system32"); + } +} + +#[cfg(not(feature = "unreserved"))] +const EXTRA_FIELD_MAPPING: [u16; 49] = [ + 0x0001, 0x0007, 0x0008, 0x0009, 0x000a, 0x000c, 0x000d, 0x000e, 0x000f, 0x0014, 0x0015, 0x0016, + 0x0017, 0x0018, 0x0019, 0x0020, 0x0021, 0x0022, 0x0023, 0x0065, 0x0066, 0x4690, 0x07c8, 0x2605, + 0x2705, 0x2805, 0x334d, 0x4341, 0x4453, 0x4704, 0x470f, 0x4b46, 0x4c41, 0x4d49, 0x4f4c, 0x5356, + 0x5455, 0x554e, 0x5855, 0x6375, 0x6542, 0x7075, 0x756e, 0x7855, 0xa11e, 0xa220, 0xfd4a, 0x9901, + 0x9902, +]; |