1 //! Types for creating ZIP archives
2
3 use crate::compression::CompressionMethod;
4 use crate::read::{central_header_to_zip_file, ZipArchive, ZipFile};
5 use crate::result::{ZipError, ZipResult};
6 use crate::spec;
7 use crate::types::{DateTime, System, ZipFileData, DEFAULT_VERSION};
8 use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
9 use crc32fast::Hasher;
10 use std::default::Default;
11 use std::io;
12 use std::io::prelude::*;
13 use std::mem;
14
15 #[cfg(any(
16 feature = "deflate",
17 feature = "deflate-miniz",
18 feature = "deflate-zlib"
19 ))]
20 use flate2::write::DeflateEncoder;
21
22 #[cfg(feature = "bzip2")]
23 use bzip2::write::BzEncoder;
24
25 enum GenericZipWriter<W: Write + io::Seek> {
26 Closed,
27 Storer(W),
28 #[cfg(any(
29 feature = "deflate",
30 feature = "deflate-miniz",
31 feature = "deflate-zlib"
32 ))]
33 Deflater(DeflateEncoder<W>),
34 #[cfg(feature = "bzip2")]
35 Bzip2(BzEncoder<W>),
36 }
37
38 /// ZIP archive generator
39 ///
40 /// Handles the bookkeeping involved in building an archive, and provides an
41 /// API to edit its contents.
42 ///
43 /// ```
44 /// # fn doit() -> zip::result::ZipResult<()>
45 /// # {
46 /// # use zip::ZipWriter;
47 /// use std::io::Write;
48 /// use zip::write::FileOptions;
49 ///
50 /// // We use a buffer here, though you'd normally use a `File`
51 /// let mut buf = [0; 65536];
52 /// let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf[..]));
53 ///
54 /// let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
55 /// zip.start_file("hello_world.txt", options)?;
56 /// zip.write(b"Hello, World!")?;
57 ///
58 /// // Apply the changes you've made.
59 /// // Dropping the `ZipWriter` will have the same effect, but may silently fail
60 /// zip.finish()?;
61 ///
62 /// # Ok(())
63 /// # }
64 /// # doit().unwrap();
65 /// ```
66 pub struct ZipWriter<W: Write + io::Seek> {
67 inner: GenericZipWriter<W>,
68 files: Vec<ZipFileData>,
69 stats: ZipWriterStats,
70 writing_to_file: bool,
71 writing_to_extra_field: bool,
72 writing_to_central_extra_field_only: bool,
73 writing_raw: bool,
74 comment: Vec<u8>,
75 }
76
77 #[derive(Default)]
78 struct ZipWriterStats {
79 hasher: Hasher,
80 start: u64,
81 bytes_written: u64,
82 }
83
84 struct ZipRawValues {
85 crc32: u32,
86 compressed_size: u64,
87 uncompressed_size: u64,
88 }
89
90 /// Metadata for a file to be written
91 #[derive(Copy, Clone)]
92 pub struct FileOptions {
93 compression_method: CompressionMethod,
94 last_modified_time: DateTime,
95 permissions: Option<u32>,
96 large_file: bool,
97 }
98
99 impl FileOptions {
100 /// Construct a new FileOptions object
default() -> FileOptions101 pub fn default() -> FileOptions {
102 FileOptions {
103 #[cfg(any(
104 feature = "deflate",
105 feature = "deflate-miniz",
106 feature = "deflate-zlib"
107 ))]
108 compression_method: CompressionMethod::Deflated,
109 #[cfg(not(any(
110 feature = "deflate",
111 feature = "deflate-miniz",
112 feature = "deflate-zlib"
113 )))]
114 compression_method: CompressionMethod::Stored,
115 #[cfg(feature = "time")]
116 last_modified_time: DateTime::from_time(time::now()).unwrap_or_default(),
117 #[cfg(not(feature = "time"))]
118 last_modified_time: DateTime::default(),
119 permissions: None,
120 large_file: false,
121 }
122 }
123
124 /// Set the compression method for the new file
125 ///
126 /// The default is `CompressionMethod::Deflated`. If the deflate compression feature is
127 /// disabled, `CompressionMethod::Stored` becomes the default.
compression_method(mut self, method: CompressionMethod) -> FileOptions128 pub fn compression_method(mut self, method: CompressionMethod) -> FileOptions {
129 self.compression_method = method;
130 self
131 }
132
133 /// Set the last modified time
134 ///
135 /// The default is the current timestamp if the 'time' feature is enabled, and 1980-01-01
136 /// otherwise
last_modified_time(mut self, mod_time: DateTime) -> FileOptions137 pub fn last_modified_time(mut self, mod_time: DateTime) -> FileOptions {
138 self.last_modified_time = mod_time;
139 self
140 }
141
142 /// Set the permissions for the new file.
143 ///
144 /// The format is represented with unix-style permissions.
145 /// The default is `0o644`, which represents `rw-r--r--` for files,
146 /// and `0o755`, which represents `rwxr-xr-x` for directories
unix_permissions(mut self, mode: u32) -> FileOptions147 pub fn unix_permissions(mut self, mode: u32) -> FileOptions {
148 self.permissions = Some(mode & 0o777);
149 self
150 }
151
152 /// Set whether the new file's compressed and uncompressed size is less than 4 GiB.
153 ///
154 /// If set to `false` and the file exceeds the limit, an I/O error is thrown. If set to `true`,
155 /// readers will require ZIP64 support and if the file does not exceed the limit, 20 B are
156 /// wasted. The default is `false`.
large_file(mut self, large: bool) -> FileOptions157 pub fn large_file(mut self, large: bool) -> FileOptions {
158 self.large_file = large;
159 self
160 }
161 }
162
163 impl Default for FileOptions {
default() -> Self164 fn default() -> Self {
165 Self::default()
166 }
167 }
168
169 impl<W: Write + io::Seek> Write for ZipWriter<W> {
write(&mut self, buf: &[u8]) -> io::Result<usize>170 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
171 if !self.writing_to_file {
172 return Err(io::Error::new(
173 io::ErrorKind::Other,
174 "No file has been started",
175 ));
176 }
177 match self.inner.ref_mut() {
178 Some(ref mut w) => {
179 if self.writing_to_extra_field {
180 self.files.last_mut().unwrap().extra_field.write(buf)
181 } else {
182 let write_result = w.write(buf);
183 if let Ok(count) = write_result {
184 self.stats.update(&buf[0..count]);
185 if self.stats.bytes_written > 0xFFFFFFFF
186 && !self.files.last_mut().unwrap().large_file
187 {
188 let _inner = mem::replace(&mut self.inner, GenericZipWriter::Closed);
189 return Err(io::Error::new(
190 io::ErrorKind::Other,
191 "Large file option has not been set",
192 ));
193 }
194 }
195 write_result
196 }
197 }
198 None => Err(io::Error::new(
199 io::ErrorKind::BrokenPipe,
200 "ZipWriter was already closed",
201 )),
202 }
203 }
204
flush(&mut self) -> io::Result<()>205 fn flush(&mut self) -> io::Result<()> {
206 match self.inner.ref_mut() {
207 Some(ref mut w) => w.flush(),
208 None => Err(io::Error::new(
209 io::ErrorKind::BrokenPipe,
210 "ZipWriter was already closed",
211 )),
212 }
213 }
214 }
215
216 impl ZipWriterStats {
update(&mut self, buf: &[u8])217 fn update(&mut self, buf: &[u8]) {
218 self.hasher.update(buf);
219 self.bytes_written += buf.len() as u64;
220 }
221 }
222
223 impl<A: Read + Write + io::Seek> ZipWriter<A> {
224 /// Initializes the archive from an existing ZIP archive, making it ready for append.
new_append(mut readwriter: A) -> ZipResult<ZipWriter<A>>225 pub fn new_append(mut readwriter: A) -> ZipResult<ZipWriter<A>> {
226 let (footer, cde_start_pos) = spec::CentralDirectoryEnd::find_and_parse(&mut readwriter)?;
227
228 if footer.disk_number != footer.disk_with_central_directory {
229 return Err(ZipError::UnsupportedArchive(
230 "Support for multi-disk files is not implemented",
231 ));
232 }
233
234 let (archive_offset, directory_start, number_of_files) =
235 ZipArchive::get_directory_counts(&mut readwriter, &footer, cde_start_pos)?;
236
237 if let Err(_) = readwriter.seek(io::SeekFrom::Start(directory_start)) {
238 return Err(ZipError::InvalidArchive(
239 "Could not seek to start of central directory",
240 ));
241 }
242
243 let files = (0..number_of_files)
244 .map(|_| central_header_to_zip_file(&mut readwriter, archive_offset))
245 .collect::<Result<Vec<_>, _>>()?;
246
247 let _ = readwriter.seek(io::SeekFrom::Start(directory_start)); // seek directory_start to overwrite it
248
249 Ok(ZipWriter {
250 inner: GenericZipWriter::Storer(readwriter),
251 files,
252 stats: Default::default(),
253 writing_to_file: false,
254 writing_to_extra_field: false,
255 writing_to_central_extra_field_only: false,
256 comment: footer.zip_file_comment,
257 writing_raw: true, // avoid recomputing the last file's header
258 })
259 }
260 }
261
262 impl<W: Write + io::Seek> ZipWriter<W> {
263 /// Initializes the archive.
264 ///
265 /// Before writing to this object, the [`ZipWriter::start_file`] function should be called.
new(inner: W) -> ZipWriter<W>266 pub fn new(inner: W) -> ZipWriter<W> {
267 ZipWriter {
268 inner: GenericZipWriter::Storer(inner),
269 files: Vec::new(),
270 stats: Default::default(),
271 writing_to_file: false,
272 writing_to_extra_field: false,
273 writing_to_central_extra_field_only: false,
274 writing_raw: false,
275 comment: Vec::new(),
276 }
277 }
278
279 /// Set ZIP archive comment.
set_comment<S>(&mut self, comment: S) where S: Into<String>,280 pub fn set_comment<S>(&mut self, comment: S)
281 where
282 S: Into<String>,
283 {
284 self.set_raw_comment(comment.into().into())
285 }
286
287 /// Set ZIP archive comment.
288 ///
289 /// This sets the raw bytes of the comment. The comment
290 /// is typically expected to be encoded in UTF-8
set_raw_comment(&mut self, comment: Vec<u8>)291 pub fn set_raw_comment(&mut self, comment: Vec<u8>) {
292 self.comment = comment;
293 }
294
295 /// Start a new file for with the requested options.
start_entry<S>( &mut self, name: S, options: FileOptions, raw_values: Option<ZipRawValues>, ) -> ZipResult<()> where S: Into<String>,296 fn start_entry<S>(
297 &mut self,
298 name: S,
299 options: FileOptions,
300 raw_values: Option<ZipRawValues>,
301 ) -> ZipResult<()>
302 where
303 S: Into<String>,
304 {
305 self.finish_file()?;
306
307 let raw_values = raw_values.unwrap_or_else(|| ZipRawValues {
308 crc32: 0,
309 compressed_size: 0,
310 uncompressed_size: 0,
311 });
312
313 {
314 let writer = self.inner.get_plain();
315 let header_start = writer.seek(io::SeekFrom::Current(0))?;
316
317 let permissions = options.permissions.unwrap_or(0o100644);
318 let mut file = ZipFileData {
319 system: System::Unix,
320 version_made_by: DEFAULT_VERSION,
321 encrypted: false,
322 using_data_descriptor: false,
323 compression_method: options.compression_method,
324 last_modified_time: options.last_modified_time,
325 crc32: raw_values.crc32,
326 compressed_size: raw_values.compressed_size,
327 uncompressed_size: raw_values.uncompressed_size,
328 file_name: name.into(),
329 file_name_raw: Vec::new(), // Never used for saving
330 extra_field: Vec::new(),
331 file_comment: String::new(),
332 header_start,
333 data_start: 0,
334 central_header_start: 0,
335 external_attributes: permissions << 16,
336 large_file: options.large_file,
337 };
338 write_local_file_header(writer, &file)?;
339
340 let header_end = writer.seek(io::SeekFrom::Current(0))?;
341 self.stats.start = header_end;
342 file.data_start = header_end;
343
344 self.stats.bytes_written = 0;
345 self.stats.hasher = Hasher::new();
346
347 self.files.push(file);
348 }
349
350 Ok(())
351 }
352
finish_file(&mut self) -> ZipResult<()>353 fn finish_file(&mut self) -> ZipResult<()> {
354 if self.writing_to_extra_field {
355 // Implicitly calling [`ZipWriter::end_extra_data`] for empty files.
356 self.end_extra_data()?;
357 }
358 self.inner.switch_to(CompressionMethod::Stored)?;
359 let writer = self.inner.get_plain();
360
361 if !self.writing_raw {
362 let file = match self.files.last_mut() {
363 None => return Ok(()),
364 Some(f) => f,
365 };
366 file.crc32 = self.stats.hasher.clone().finalize();
367 file.uncompressed_size = self.stats.bytes_written;
368
369 let file_end = writer.seek(io::SeekFrom::Current(0))?;
370 file.compressed_size = file_end - self.stats.start;
371
372 update_local_file_header(writer, file)?;
373 writer.seek(io::SeekFrom::Start(file_end))?;
374 }
375
376 self.writing_to_file = false;
377 self.writing_raw = false;
378 Ok(())
379 }
380
381 /// Create a file in the archive and start writing its' contents.
382 ///
383 /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`]
start_file<S>(&mut self, name: S, mut options: FileOptions) -> ZipResult<()> where S: Into<String>,384 pub fn start_file<S>(&mut self, name: S, mut options: FileOptions) -> ZipResult<()>
385 where
386 S: Into<String>,
387 {
388 if options.permissions.is_none() {
389 options.permissions = Some(0o644);
390 }
391 *options.permissions.as_mut().unwrap() |= 0o100000;
392 self.start_entry(name, options, None)?;
393 self.inner.switch_to(options.compression_method)?;
394 self.writing_to_file = true;
395 Ok(())
396 }
397
398 /// Starts a file, taking a Path as argument.
399 ///
400 /// This function ensures that the '/' path separator is used. It also ignores all non 'Normal'
401 /// Components, such as a starting '/' or '..' and '.'.
402 #[deprecated(
403 since = "0.5.7",
404 note = "by stripping `..`s from the path, the meaning of paths can change. Use `start_file` instead."
405 )]
start_file_from_path( &mut self, path: &std::path::Path, options: FileOptions, ) -> ZipResult<()>406 pub fn start_file_from_path(
407 &mut self,
408 path: &std::path::Path,
409 options: FileOptions,
410 ) -> ZipResult<()> {
411 self.start_file(path_to_string(path), options)
412 }
413
414 /// Create an aligned file in the archive and start writing its' contents.
415 ///
416 /// Returns the number of padding bytes required to align the file.
417 ///
418 /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`]
start_file_aligned<S>( &mut self, name: S, options: FileOptions, align: u16, ) -> Result<u64, ZipError> where S: Into<String>,419 pub fn start_file_aligned<S>(
420 &mut self,
421 name: S,
422 options: FileOptions,
423 align: u16,
424 ) -> Result<u64, ZipError>
425 where
426 S: Into<String>,
427 {
428 let data_start = self.start_file_with_extra_data(name, options)?;
429 let align = align as u64;
430 if align > 1 && data_start % align != 0 {
431 let pad_length = (align - (data_start + 4) % align) % align;
432 let pad = vec![0; pad_length as usize];
433 self.write_all(b"za").map_err(ZipError::from)?; // 0x617a
434 self.write_u16::<LittleEndian>(pad.len() as u16)
435 .map_err(ZipError::from)?;
436 self.write_all(&pad).map_err(ZipError::from)?;
437 assert_eq!(self.end_local_start_central_extra_data()? % align, 0);
438 }
439 let extra_data_end = self.end_extra_data()?;
440 Ok(extra_data_end - data_start)
441 }
442
443 /// Create a file in the archive and start writing its extra data first.
444 ///
445 /// Finish writing extra data and start writing file data with [`ZipWriter::end_extra_data`].
446 /// Optionally, distinguish local from central extra data with
447 /// [`ZipWriter::end_local_start_central_extra_data`].
448 ///
449 /// Returns the preliminary starting offset of the file data without any extra data allowing to
450 /// align the file data by calculating a pad length to be prepended as part of the extra data.
451 ///
452 /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`]
453 ///
454 /// ```
455 /// use byteorder::{LittleEndian, WriteBytesExt};
456 /// use zip::{ZipArchive, ZipWriter, result::ZipResult};
457 /// use zip::{write::FileOptions, CompressionMethod};
458 /// use std::io::{Write, Cursor};
459 ///
460 /// # fn main() -> ZipResult<()> {
461 /// let mut archive = Cursor::new(Vec::new());
462 ///
463 /// {
464 /// let mut zip = ZipWriter::new(&mut archive);
465 /// let options = FileOptions::default()
466 /// .compression_method(CompressionMethod::Stored);
467 ///
468 /// zip.start_file_with_extra_data("identical_extra_data.txt", options)?;
469 /// let extra_data = b"local and central extra data";
470 /// zip.write_u16::<LittleEndian>(0xbeef)?;
471 /// zip.write_u16::<LittleEndian>(extra_data.len() as u16)?;
472 /// zip.write_all(extra_data)?;
473 /// zip.end_extra_data()?;
474 /// zip.write_all(b"file data")?;
475 ///
476 /// let data_start = zip.start_file_with_extra_data("different_extra_data.txt", options)?;
477 /// let extra_data = b"local extra data";
478 /// zip.write_u16::<LittleEndian>(0xbeef)?;
479 /// zip.write_u16::<LittleEndian>(extra_data.len() as u16)?;
480 /// zip.write_all(extra_data)?;
481 /// let data_start = data_start as usize + 4 + extra_data.len() + 4;
482 /// let align = 64;
483 /// let pad_length = (align - data_start % align) % align;
484 /// assert_eq!(pad_length, 19);
485 /// zip.write_u16::<LittleEndian>(0xdead)?;
486 /// zip.write_u16::<LittleEndian>(pad_length as u16)?;
487 /// zip.write_all(&vec![0; pad_length])?;
488 /// let data_start = zip.end_local_start_central_extra_data()?;
489 /// assert_eq!(data_start as usize % align, 0);
490 /// let extra_data = b"central extra data";
491 /// zip.write_u16::<LittleEndian>(0xbeef)?;
492 /// zip.write_u16::<LittleEndian>(extra_data.len() as u16)?;
493 /// zip.write_all(extra_data)?;
494 /// zip.end_extra_data()?;
495 /// zip.write_all(b"file data")?;
496 ///
497 /// zip.finish()?;
498 /// }
499 ///
500 /// let mut zip = ZipArchive::new(archive)?;
501 /// assert_eq!(&zip.by_index(0)?.extra_data()[4..], b"local and central extra data");
502 /// assert_eq!(&zip.by_index(1)?.extra_data()[4..], b"central extra data");
503 /// # Ok(())
504 /// # }
505 /// ```
start_file_with_extra_data<S>( &mut self, name: S, mut options: FileOptions, ) -> ZipResult<u64> where S: Into<String>,506 pub fn start_file_with_extra_data<S>(
507 &mut self,
508 name: S,
509 mut options: FileOptions,
510 ) -> ZipResult<u64>
511 where
512 S: Into<String>,
513 {
514 if options.permissions.is_none() {
515 options.permissions = Some(0o644);
516 }
517 *options.permissions.as_mut().unwrap() |= 0o100000;
518 self.start_entry(name, options, None)?;
519 self.writing_to_file = true;
520 self.writing_to_extra_field = true;
521 Ok(self.files.last().unwrap().data_start)
522 }
523
524 /// End local and start central extra data. Requires [`ZipWriter::start_file_with_extra_data`].
525 ///
526 /// Returns the final starting offset of the file data.
end_local_start_central_extra_data(&mut self) -> ZipResult<u64>527 pub fn end_local_start_central_extra_data(&mut self) -> ZipResult<u64> {
528 let data_start = self.end_extra_data()?;
529 self.files.last_mut().unwrap().extra_field.clear();
530 self.writing_to_extra_field = true;
531 self.writing_to_central_extra_field_only = true;
532 Ok(data_start)
533 }
534
535 /// End extra data and start file data. Requires [`ZipWriter::start_file_with_extra_data`].
536 ///
537 /// Returns the final starting offset of the file data.
end_extra_data(&mut self) -> ZipResult<u64>538 pub fn end_extra_data(&mut self) -> ZipResult<u64> {
539 // Require `start_file_with_extra_data()`. Ensures `file` is some.
540 if !self.writing_to_extra_field {
541 return Err(ZipError::Io(io::Error::new(
542 io::ErrorKind::Other,
543 "Not writing to extra field",
544 )));
545 }
546 let file = self.files.last_mut().unwrap();
547
548 validate_extra_data(&file)?;
549
550 if !self.writing_to_central_extra_field_only {
551 let writer = self.inner.get_plain();
552
553 // Append extra data to local file header and keep it for central file header.
554 writer.write_all(&file.extra_field)?;
555
556 // Update final `data_start`.
557 let header_end = file.data_start + file.extra_field.len() as u64;
558 self.stats.start = header_end;
559 file.data_start = header_end;
560
561 // Update extra field length in local file header.
562 let extra_field_length =
563 if file.large_file { 20 } else { 0 } + file.extra_field.len() as u16;
564 writer.seek(io::SeekFrom::Start(file.header_start + 28))?;
565 writer.write_u16::<LittleEndian>(extra_field_length)?;
566 writer.seek(io::SeekFrom::Start(header_end))?;
567
568 self.inner.switch_to(file.compression_method)?;
569 }
570
571 self.writing_to_extra_field = false;
572 self.writing_to_central_extra_field_only = false;
573 Ok(file.data_start)
574 }
575
576 /// Add a new file using the already compressed data from a ZIP file being read and renames it, this
577 /// allows faster copies of the `ZipFile` since there is no need to decompress and compress it again.
578 /// Any `ZipFile` metadata is copied and not checked, for example the file CRC.
579
580 /// ```no_run
581 /// use std::fs::File;
582 /// use std::io::{Read, Seek, Write};
583 /// use zip::{ZipArchive, ZipWriter};
584 ///
585 /// fn copy_rename<R, W>(
586 /// src: &mut ZipArchive<R>,
587 /// dst: &mut ZipWriter<W>,
588 /// ) -> zip::result::ZipResult<()>
589 /// where
590 /// R: Read + Seek,
591 /// W: Write + Seek,
592 /// {
593 /// // Retrieve file entry by name
594 /// let file = src.by_name("src_file.txt")?;
595 ///
596 /// // Copy and rename the previously obtained file entry to the destination zip archive
597 /// dst.raw_copy_file_rename(file, "new_name.txt")?;
598 ///
599 /// Ok(())
600 /// }
601 /// ```
raw_copy_file_rename<S>(&mut self, mut file: ZipFile, name: S) -> ZipResult<()> where S: Into<String>,602 pub fn raw_copy_file_rename<S>(&mut self, mut file: ZipFile, name: S) -> ZipResult<()>
603 where
604 S: Into<String>,
605 {
606 let options = FileOptions::default()
607 .last_modified_time(file.last_modified())
608 .compression_method(file.compression());
609 if let Some(perms) = file.unix_mode() {
610 options.unix_permissions(perms);
611 }
612
613 let raw_values = ZipRawValues {
614 crc32: file.crc32(),
615 compressed_size: file.compressed_size(),
616 uncompressed_size: file.size(),
617 };
618
619 self.start_entry(name, options, Some(raw_values))?;
620 self.writing_to_file = true;
621 self.writing_raw = true;
622
623 io::copy(file.get_raw_reader(), self)?;
624
625 Ok(())
626 }
627
628 /// Add a new file using the already compressed data from a ZIP file being read, this allows faster
629 /// copies of the `ZipFile` since there is no need to decompress and compress it again. Any `ZipFile`
630 /// metadata is copied and not checked, for example the file CRC.
631 ///
632 /// ```no_run
633 /// use std::fs::File;
634 /// use std::io::{Read, Seek, Write};
635 /// use zip::{ZipArchive, ZipWriter};
636 ///
637 /// fn copy<R, W>(src: &mut ZipArchive<R>, dst: &mut ZipWriter<W>) -> zip::result::ZipResult<()>
638 /// where
639 /// R: Read + Seek,
640 /// W: Write + Seek,
641 /// {
642 /// // Retrieve file entry by name
643 /// let file = src.by_name("src_file.txt")?;
644 ///
645 /// // Copy the previously obtained file entry to the destination zip archive
646 /// dst.raw_copy_file(file)?;
647 ///
648 /// Ok(())
649 /// }
650 /// ```
raw_copy_file(&mut self, file: ZipFile) -> ZipResult<()>651 pub fn raw_copy_file(&mut self, file: ZipFile) -> ZipResult<()> {
652 let name = file.name().to_owned();
653 self.raw_copy_file_rename(file, name)
654 }
655
656 /// Add a directory entry.
657 ///
658 /// You can't write data to the file afterwards.
add_directory<S>(&mut self, name: S, mut options: FileOptions) -> ZipResult<()> where S: Into<String>,659 pub fn add_directory<S>(&mut self, name: S, mut options: FileOptions) -> ZipResult<()>
660 where
661 S: Into<String>,
662 {
663 if options.permissions.is_none() {
664 options.permissions = Some(0o755);
665 }
666 *options.permissions.as_mut().unwrap() |= 0o40000;
667 options.compression_method = CompressionMethod::Stored;
668
669 let name_as_string = name.into();
670 // Append a slash to the filename if it does not end with it.
671 let name_with_slash = match name_as_string.chars().last() {
672 Some('/') | Some('\\') => name_as_string,
673 _ => name_as_string + "/",
674 };
675
676 self.start_entry(name_with_slash, options, None)?;
677 self.writing_to_file = false;
678 Ok(())
679 }
680
681 /// Add a directory entry, taking a Path as argument.
682 ///
683 /// This function ensures that the '/' path seperator is used. It also ignores all non 'Normal'
684 /// Components, such as a starting '/' or '..' and '.'.
685 #[deprecated(
686 since = "0.5.7",
687 note = "by stripping `..`s from the path, the meaning of paths can change. Use `add_directory` instead."
688 )]
add_directory_from_path( &mut self, path: &std::path::Path, options: FileOptions, ) -> ZipResult<()>689 pub fn add_directory_from_path(
690 &mut self,
691 path: &std::path::Path,
692 options: FileOptions,
693 ) -> ZipResult<()> {
694 self.add_directory(path_to_string(path), options)
695 }
696
697 /// Finish the last file and write all other zip-structures
698 ///
699 /// This will return the writer, but one should normally not append any data to the end of the file.
700 /// Note that the zipfile will also be finished on drop.
finish(&mut self) -> ZipResult<W>701 pub fn finish(&mut self) -> ZipResult<W> {
702 self.finalize()?;
703 let inner = mem::replace(&mut self.inner, GenericZipWriter::Closed);
704 Ok(inner.unwrap())
705 }
706
finalize(&mut self) -> ZipResult<()>707 fn finalize(&mut self) -> ZipResult<()> {
708 self.finish_file()?;
709
710 {
711 let writer = self.inner.get_plain();
712
713 let central_start = writer.seek(io::SeekFrom::Current(0))?;
714 for file in self.files.iter() {
715 write_central_directory_header(writer, file)?;
716 }
717 let central_size = writer.seek(io::SeekFrom::Current(0))? - central_start;
718
719 if self.files.len() > 0xFFFF || central_size > 0xFFFFFFFF || central_start > 0xFFFFFFFF
720 {
721 let zip64_footer = spec::Zip64CentralDirectoryEnd {
722 version_made_by: DEFAULT_VERSION as u16,
723 version_needed_to_extract: DEFAULT_VERSION as u16,
724 disk_number: 0,
725 disk_with_central_directory: 0,
726 number_of_files_on_this_disk: self.files.len() as u64,
727 number_of_files: self.files.len() as u64,
728 central_directory_size: central_size,
729 central_directory_offset: central_start,
730 };
731
732 zip64_footer.write(writer)?;
733
734 let zip64_footer = spec::Zip64CentralDirectoryEndLocator {
735 disk_with_central_directory: 0,
736 end_of_central_directory_offset: central_start + central_size,
737 number_of_disks: 1,
738 };
739
740 zip64_footer.write(writer)?;
741 }
742
743 let number_of_files = if self.files.len() > 0xFFFF {
744 0xFFFF
745 } else {
746 self.files.len() as u16
747 };
748 let footer = spec::CentralDirectoryEnd {
749 disk_number: 0,
750 disk_with_central_directory: 0,
751 zip_file_comment: self.comment.clone(),
752 number_of_files_on_this_disk: number_of_files,
753 number_of_files,
754 central_directory_size: if central_size > 0xFFFFFFFF {
755 0xFFFFFFFF
756 } else {
757 central_size as u32
758 },
759 central_directory_offset: if central_start > 0xFFFFFFFF {
760 0xFFFFFFFF
761 } else {
762 central_start as u32
763 },
764 };
765
766 footer.write(writer)?;
767 }
768
769 Ok(())
770 }
771 }
772
773 impl<W: Write + io::Seek> Drop for ZipWriter<W> {
drop(&mut self)774 fn drop(&mut self) {
775 if !self.inner.is_closed() {
776 if let Err(e) = self.finalize() {
777 let _ = write!(&mut io::stderr(), "ZipWriter drop failed: {:?}", e);
778 }
779 }
780 }
781 }
782
783 impl<W: Write + io::Seek> GenericZipWriter<W> {
switch_to(&mut self, compression: CompressionMethod) -> ZipResult<()>784 fn switch_to(&mut self, compression: CompressionMethod) -> ZipResult<()> {
785 match self.current_compression() {
786 Some(method) if method == compression => return Ok(()),
787 None => {
788 return Err(io::Error::new(
789 io::ErrorKind::BrokenPipe,
790 "ZipWriter was already closed",
791 )
792 .into())
793 }
794 _ => {}
795 }
796
797 let bare = match mem::replace(self, GenericZipWriter::Closed) {
798 GenericZipWriter::Storer(w) => w,
799 #[cfg(any(
800 feature = "deflate",
801 feature = "deflate-miniz",
802 feature = "deflate-zlib"
803 ))]
804 GenericZipWriter::Deflater(w) => w.finish()?,
805 #[cfg(feature = "bzip2")]
806 GenericZipWriter::Bzip2(w) => w.finish()?,
807 GenericZipWriter::Closed => {
808 return Err(io::Error::new(
809 io::ErrorKind::BrokenPipe,
810 "ZipWriter was already closed",
811 )
812 .into())
813 }
814 };
815
816 *self = {
817 #[allow(deprecated)]
818 match compression {
819 CompressionMethod::Stored => GenericZipWriter::Storer(bare),
820 #[cfg(any(
821 feature = "deflate",
822 feature = "deflate-miniz",
823 feature = "deflate-zlib"
824 ))]
825 CompressionMethod::Deflated => GenericZipWriter::Deflater(DeflateEncoder::new(
826 bare,
827 flate2::Compression::default(),
828 )),
829 #[cfg(feature = "bzip2")]
830 CompressionMethod::Bzip2 => {
831 GenericZipWriter::Bzip2(BzEncoder::new(bare, bzip2::Compression::default()))
832 }
833 CompressionMethod::Unsupported(..) => {
834 return Err(ZipError::UnsupportedArchive("Unsupported compression"))
835 }
836 }
837 };
838
839 Ok(())
840 }
841
ref_mut(&mut self) -> Option<&mut dyn Write>842 fn ref_mut(&mut self) -> Option<&mut dyn Write> {
843 match *self {
844 GenericZipWriter::Storer(ref mut w) => Some(w as &mut dyn Write),
845 #[cfg(any(
846 feature = "deflate",
847 feature = "deflate-miniz",
848 feature = "deflate-zlib"
849 ))]
850 GenericZipWriter::Deflater(ref mut w) => Some(w as &mut dyn Write),
851 #[cfg(feature = "bzip2")]
852 GenericZipWriter::Bzip2(ref mut w) => Some(w as &mut dyn Write),
853 GenericZipWriter::Closed => None,
854 }
855 }
856
is_closed(&self) -> bool857 fn is_closed(&self) -> bool {
858 match *self {
859 GenericZipWriter::Closed => true,
860 _ => false,
861 }
862 }
863
get_plain(&mut self) -> &mut W864 fn get_plain(&mut self) -> &mut W {
865 match *self {
866 GenericZipWriter::Storer(ref mut w) => w,
867 _ => panic!("Should have switched to stored beforehand"),
868 }
869 }
870
current_compression(&self) -> Option<CompressionMethod>871 fn current_compression(&self) -> Option<CompressionMethod> {
872 match *self {
873 GenericZipWriter::Storer(..) => Some(CompressionMethod::Stored),
874 #[cfg(any(
875 feature = "deflate",
876 feature = "deflate-miniz",
877 feature = "deflate-zlib"
878 ))]
879 GenericZipWriter::Deflater(..) => Some(CompressionMethod::Deflated),
880 #[cfg(feature = "bzip2")]
881 GenericZipWriter::Bzip2(..) => Some(CompressionMethod::Bzip2),
882 GenericZipWriter::Closed => None,
883 }
884 }
885
unwrap(self) -> W886 fn unwrap(self) -> W {
887 match self {
888 GenericZipWriter::Storer(w) => w,
889 _ => panic!("Should have switched to stored beforehand"),
890 }
891 }
892 }
893
write_local_file_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()>894 fn write_local_file_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
895 // local file header signature
896 writer.write_u32::<LittleEndian>(spec::LOCAL_FILE_HEADER_SIGNATURE)?;
897 // version needed to extract
898 writer.write_u16::<LittleEndian>(file.version_needed())?;
899 // general purpose bit flag
900 let flag = if !file.file_name.is_ascii() {
901 1u16 << 11
902 } else {
903 0
904 };
905 writer.write_u16::<LittleEndian>(flag)?;
906 // Compression method
907 #[allow(deprecated)]
908 writer.write_u16::<LittleEndian>(file.compression_method.to_u16())?;
909 // last mod file time and last mod file date
910 writer.write_u16::<LittleEndian>(file.last_modified_time.timepart())?;
911 writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?;
912 // crc-32
913 writer.write_u32::<LittleEndian>(file.crc32)?;
914 // compressed size
915 writer.write_u32::<LittleEndian>(if file.compressed_size > 0xFFFFFFFF {
916 0xFFFFFFFF
917 } else {
918 file.compressed_size as u32
919 })?;
920 // uncompressed size
921 writer.write_u32::<LittleEndian>(if file.uncompressed_size > 0xFFFFFFFF {
922 0xFFFFFFFF
923 } else {
924 file.uncompressed_size as u32
925 })?;
926 // file name length
927 writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
928 // extra field length
929 let extra_field_length = if file.large_file { 20 } else { 0 } + file.extra_field.len() as u16;
930 writer.write_u16::<LittleEndian>(extra_field_length)?;
931 // file name
932 writer.write_all(file.file_name.as_bytes())?;
933 // zip64 extra field
934 if file.large_file {
935 write_local_zip64_extra_field(writer, &file)?;
936 }
937
938 Ok(())
939 }
940
update_local_file_header<T: Write + io::Seek>( writer: &mut T, file: &ZipFileData, ) -> ZipResult<()>941 fn update_local_file_header<T: Write + io::Seek>(
942 writer: &mut T,
943 file: &ZipFileData,
944 ) -> ZipResult<()> {
945 const CRC32_OFFSET: u64 = 14;
946 writer.seek(io::SeekFrom::Start(file.header_start + CRC32_OFFSET))?;
947 writer.write_u32::<LittleEndian>(file.crc32)?;
948 writer.write_u32::<LittleEndian>(if file.compressed_size > 0xFFFFFFFF {
949 if file.large_file {
950 0xFFFFFFFF
951 } else {
952 // compressed size can be slightly larger than uncompressed size
953 return Err(ZipError::Io(io::Error::new(
954 io::ErrorKind::Other,
955 "Large file option has not been set",
956 )));
957 }
958 } else {
959 file.compressed_size as u32
960 })?;
961 writer.write_u32::<LittleEndian>(if file.uncompressed_size > 0xFFFFFFFF {
962 // uncompressed size is checked on write to catch it as soon as possible
963 0xFFFFFFFF
964 } else {
965 file.uncompressed_size as u32
966 })?;
967 if file.large_file {
968 update_local_zip64_extra_field(writer, file)?;
969 }
970 Ok(())
971 }
972
write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()>973 fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
974 // buffer zip64 extra field to determine its variable length
975 let mut zip64_extra_field = [0; 28];
976 let zip64_extra_field_length =
977 write_central_zip64_extra_field(&mut zip64_extra_field.as_mut(), file)?;
978
979 // central file header signature
980 writer.write_u32::<LittleEndian>(spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE)?;
981 // version made by
982 let version_made_by = (file.system as u16) << 8 | (file.version_made_by as u16);
983 writer.write_u16::<LittleEndian>(version_made_by)?;
984 // version needed to extract
985 writer.write_u16::<LittleEndian>(file.version_needed())?;
986 // general puprose bit flag
987 let flag = if !file.file_name.is_ascii() {
988 1u16 << 11
989 } else {
990 0
991 };
992 writer.write_u16::<LittleEndian>(flag)?;
993 // compression method
994 #[allow(deprecated)]
995 writer.write_u16::<LittleEndian>(file.compression_method.to_u16())?;
996 // last mod file time + date
997 writer.write_u16::<LittleEndian>(file.last_modified_time.timepart())?;
998 writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?;
999 // crc-32
1000 writer.write_u32::<LittleEndian>(file.crc32)?;
1001 // compressed size
1002 writer.write_u32::<LittleEndian>(if file.compressed_size > 0xFFFFFFFF {
1003 0xFFFFFFFF
1004 } else {
1005 file.compressed_size as u32
1006 })?;
1007 // uncompressed size
1008 writer.write_u32::<LittleEndian>(if file.uncompressed_size > 0xFFFFFFFF {
1009 0xFFFFFFFF
1010 } else {
1011 file.uncompressed_size as u32
1012 })?;
1013 // file name length
1014 writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
1015 // extra field length
1016 writer.write_u16::<LittleEndian>(zip64_extra_field_length + file.extra_field.len() as u16)?;
1017 // file comment length
1018 writer.write_u16::<LittleEndian>(0)?;
1019 // disk number start
1020 writer.write_u16::<LittleEndian>(0)?;
1021 // internal file attribytes
1022 writer.write_u16::<LittleEndian>(0)?;
1023 // external file attributes
1024 writer.write_u32::<LittleEndian>(file.external_attributes)?;
1025 // relative offset of local header
1026 writer.write_u32::<LittleEndian>(if file.header_start > 0xFFFFFFFF {
1027 0xFFFFFFFF
1028 } else {
1029 file.header_start as u32
1030 })?;
1031 // file name
1032 writer.write_all(file.file_name.as_bytes())?;
1033 // zip64 extra field
1034 writer.write_all(&zip64_extra_field[..zip64_extra_field_length as usize])?;
1035 // extra field
1036 writer.write_all(&file.extra_field)?;
1037 // file comment
1038 // <none>
1039
1040 Ok(())
1041 }
1042
validate_extra_data(file: &ZipFileData) -> ZipResult<()>1043 fn validate_extra_data(file: &ZipFileData) -> ZipResult<()> {
1044 let mut data = file.extra_field.as_slice();
1045
1046 if data.len() > 0xFFFF {
1047 return Err(ZipError::Io(io::Error::new(
1048 io::ErrorKind::InvalidData,
1049 "Extra data exceeds extra field",
1050 )));
1051 }
1052
1053 while data.len() > 0 {
1054 let left = data.len();
1055 if left < 4 {
1056 return Err(ZipError::Io(io::Error::new(
1057 io::ErrorKind::Other,
1058 "Incomplete extra data header",
1059 )));
1060 }
1061 let kind = data.read_u16::<LittleEndian>()?;
1062 let size = data.read_u16::<LittleEndian>()? as usize;
1063 let left = left - 4;
1064
1065 if kind == 0x0001 {
1066 return Err(ZipError::Io(io::Error::new(
1067 io::ErrorKind::Other,
1068 "No custom ZIP64 extra data allowed",
1069 )));
1070 }
1071
1072 #[cfg(not(feature = "unreserved"))]
1073 {
1074 if kind <= 31 || EXTRA_FIELD_MAPPING.iter().any(|&mapped| mapped == kind) {
1075 return Err(ZipError::Io(io::Error::new(
1076 io::ErrorKind::Other,
1077 format!(
1078 "Extra data header ID {:#06} requires crate feature \"unreserved\"",
1079 kind,
1080 ),
1081 )));
1082 }
1083 }
1084
1085 if size > left {
1086 return Err(ZipError::Io(io::Error::new(
1087 io::ErrorKind::Other,
1088 "Extra data size exceeds extra field",
1089 )));
1090 }
1091
1092 data = &data[size..];
1093 }
1094
1095 Ok(())
1096 }
1097
write_local_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()>1098 fn write_local_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
1099 // This entry in the Local header MUST include BOTH original
1100 // and compressed file size fields.
1101 writer.write_u16::<LittleEndian>(0x0001)?;
1102 writer.write_u16::<LittleEndian>(16)?;
1103 writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
1104 writer.write_u64::<LittleEndian>(file.compressed_size)?;
1105 // Excluded fields:
1106 // u32: disk start number
1107 Ok(())
1108 }
1109
update_local_zip64_extra_field<T: Write + io::Seek>( writer: &mut T, file: &ZipFileData, ) -> ZipResult<()>1110 fn update_local_zip64_extra_field<T: Write + io::Seek>(
1111 writer: &mut T,
1112 file: &ZipFileData,
1113 ) -> ZipResult<()> {
1114 let zip64_extra_field = file.header_start + 30 + file.file_name.as_bytes().len() as u64;
1115 writer.seek(io::SeekFrom::Start(zip64_extra_field + 4))?;
1116 writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
1117 writer.write_u64::<LittleEndian>(file.compressed_size)?;
1118 // Excluded fields:
1119 // u32: disk start number
1120 Ok(())
1121 }
1122
write_central_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<u16>1123 fn write_central_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<u16> {
1124 // The order of the fields in the zip64 extended
1125 // information record is fixed, but the fields MUST
1126 // only appear if the corresponding Local or Central
1127 // directory record field is set to 0xFFFF or 0xFFFFFFFF.
1128 let mut size = 0;
1129 let uncompressed_size = file.uncompressed_size > 0xFFFFFFFF;
1130 let compressed_size = file.compressed_size > 0xFFFFFFFF;
1131 let header_start = file.header_start > 0xFFFFFFFF;
1132 if uncompressed_size {
1133 size += 8;
1134 }
1135 if compressed_size {
1136 size += 8;
1137 }
1138 if header_start {
1139 size += 8;
1140 }
1141 if size > 0 {
1142 writer.write_u16::<LittleEndian>(0x0001)?;
1143 writer.write_u16::<LittleEndian>(size)?;
1144 size += 4;
1145
1146 if uncompressed_size {
1147 writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
1148 }
1149 if compressed_size {
1150 writer.write_u64::<LittleEndian>(file.compressed_size)?;
1151 }
1152 if header_start {
1153 writer.write_u64::<LittleEndian>(file.header_start)?;
1154 }
1155 // Excluded fields:
1156 // u32: disk start number
1157 }
1158 Ok(size)
1159 }
1160
path_to_string(path: &std::path::Path) -> String1161 fn path_to_string(path: &std::path::Path) -> String {
1162 let mut path_str = String::new();
1163 for component in path.components() {
1164 if let std::path::Component::Normal(os_str) = component {
1165 if !path_str.is_empty() {
1166 path_str.push('/');
1167 }
1168 path_str.push_str(&*os_str.to_string_lossy());
1169 }
1170 }
1171 path_str
1172 }
1173
1174 #[cfg(test)]
1175 mod test {
1176 use super::{FileOptions, ZipWriter};
1177 use crate::compression::CompressionMethod;
1178 use crate::types::DateTime;
1179 use std::io;
1180 use std::io::Write;
1181
1182 #[test]
write_empty_zip()1183 fn write_empty_zip() {
1184 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1185 writer.set_comment("ZIP");
1186 let result = writer.finish().unwrap();
1187 assert_eq!(result.get_ref().len(), 25);
1188 assert_eq!(
1189 *result.get_ref(),
1190 [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]
1191 );
1192 }
1193
1194 #[test]
write_zip_dir()1195 fn write_zip_dir() {
1196 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1197 writer
1198 .add_directory(
1199 "test",
1200 FileOptions::default().last_modified_time(
1201 DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
1202 ),
1203 )
1204 .unwrap();
1205 assert!(writer
1206 .write(b"writing to a directory is not allowed, and will not write any data")
1207 .is_err());
1208 let result = writer.finish().unwrap();
1209 assert_eq!(result.get_ref().len(), 108);
1210 assert_eq!(
1211 *result.get_ref(),
1212 &[
1213 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1214 0, 0, 5, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 1, 2, 46, 3, 20, 0, 0, 0, 0, 0,
1215 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,
1216 0, 0, 237, 65, 0, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0,
1217 1, 0, 51, 0, 0, 0, 35, 0, 0, 0, 0, 0,
1218 ] as &[u8]
1219 );
1220 }
1221
1222 #[test]
write_mimetype_zip()1223 fn write_mimetype_zip() {
1224 let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
1225 let options = FileOptions {
1226 compression_method: CompressionMethod::Stored,
1227 last_modified_time: DateTime::default(),
1228 permissions: Some(33188),
1229 large_file: false,
1230 };
1231 writer.start_file("mimetype", options).unwrap();
1232 writer
1233 .write(b"application/vnd.oasis.opendocument.text")
1234 .unwrap();
1235 let result = writer.finish().unwrap();
1236
1237 assert_eq!(result.get_ref().len(), 153);
1238 let mut v = Vec::new();
1239 v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip"));
1240 assert_eq!(result.get_ref(), &v);
1241 }
1242
1243 #[test]
path_to_string()1244 fn path_to_string() {
1245 let mut path = std::path::PathBuf::new();
1246 #[cfg(windows)]
1247 path.push(r"C:\");
1248 #[cfg(unix)]
1249 path.push("/");
1250 path.push("windows");
1251 path.push("..");
1252 path.push(".");
1253 path.push("system32");
1254 let path_str = super::path_to_string(&path);
1255 assert_eq!(path_str, "windows/system32");
1256 }
1257 }
1258
1259 #[cfg(not(feature = "unreserved"))]
1260 const EXTRA_FIELD_MAPPING: [u16; 49] = [
1261 0x0001, 0x0007, 0x0008, 0x0009, 0x000a, 0x000c, 0x000d, 0x000e, 0x000f, 0x0014, 0x0015, 0x0016,
1262 0x0017, 0x0018, 0x0019, 0x0020, 0x0021, 0x0022, 0x0023, 0x0065, 0x0066, 0x4690, 0x07c8, 0x2605,
1263 0x2705, 0x2805, 0x334d, 0x4341, 0x4453, 0x4704, 0x470f, 0x4b46, 0x4c41, 0x4d49, 0x4f4c, 0x5356,
1264 0x5455, 0x554e, 0x5855, 0x6375, 0x6542, 0x7075, 0x756e, 0x7855, 0xa11e, 0xa220, 0xfd4a, 0x9901,
1265 0x9902,
1266 ];
1267