3 //! Provides an interface to create a ZIP File from ZipEntries
4 //! for a more detailed description of the ZIP format, see:
5 //! https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
7 use std
::convert
::TryInto
;
8 use std
::ffi
::OsString
;
10 use std
::mem
::size_of
;
11 use std
::os
::unix
::ffi
::OsStrExt
;
12 use std
::path
::{Component, Path, PathBuf}
;
14 use std
::task
::{Context, Poll}
;
15 use std
::time
::SystemTime
;
17 use anyhow
::{format_err, Error, Result}
;
18 use endian_trait
::Endian
;
20 use tokio
::io
::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf}
;
22 use crc32fast
::Hasher
;
23 use proxmox_time
::gmtime
;
25 use crate::compression
::{DeflateEncoder, Level}
;
27 const LOCAL_FH_SIG
: u32 = 0x04034B50;
28 const LOCAL_FF_SIG
: u32 = 0x08074B50;
29 const CENTRAL_DIRECTORY_FH_SIG
: u32 = 0x02014B50;
30 const END_OF_CENTRAL_DIR
: u32 = 0x06054B50;
31 const VERSION_NEEDED
: u16 = 0x002d;
32 const VERSION_MADE_BY
: u16 = 0x032d;
34 const ZIP64_EOCD_RECORD
: u32 = 0x06064B50;
35 const ZIP64_EOCD_LOCATOR
: u32 = 0x07064B50;
38 // 0-4: day of the month (1-31)
39 // 5-8: month: (1 = jan, etc.)
40 // 9-15: year offset from 1980
44 // 5-10: minute (0-59)
47 // see https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-filetimetodosdatetime
48 fn epoch_to_dos(epoch
: i64) -> (u16, u16) {
49 let gmtime
= match gmtime(epoch
) {
51 Err(_
) => return (0, 0),
54 let seconds
= (gmtime
.tm_sec
/ 2) & 0b11111;
55 let minutes
= gmtime
.tm_min
& 0xb111111;
56 let hours
= gmtime
.tm_hour
& 0b11111;
57 let time
: u16 = ((hours
<< 11) | (minutes
<< 5) | (seconds
)) as u16;
59 let date
: u16 = if gmtime
.tm_year
> (2108 - 1900) || gmtime
.tm_year
< (1980 - 1900) {
62 let day
= gmtime
.tm_mday
& 0b11111;
63 let month
= (gmtime
.tm_mon
+ 1) & 0b1111;
64 let year
= (gmtime
.tm_year
+ 1900 - 1980) & 0b1111111;
65 ((year
<< 9) | (month
<< 5) | (day
)) as u16
76 uncompressed_size
: u64,
82 struct Zip64FieldWithOffset
{
85 uncompressed_size
: u64,
93 struct LocalFileHeader
{
101 compressed_size
: u32,
102 uncompressed_size
: u32,
104 extra_field_len
: u16,
109 struct LocalFileFooter
{
112 compressed_size
: u64,
113 uncompressed_size
: u64,
118 struct CentralDirectoryFileHeader
{
120 version_made_by
: u16,
127 compressed_size
: u32,
128 uncompressed_size
: u32,
130 extra_field_len
: u16,
140 struct EndOfCentralDir
{
144 disk_record_count
: u16,
145 total_record_count
: u16,
147 directory_offset
: u32,
153 struct Zip64EOCDRecord
{
156 version_made_by
: u16,
159 disk_number_central_dir
: u32,
160 disk_record_count
: u64,
161 total_record_count
: u64,
163 directory_offset
: u64,
168 struct Zip64EOCDLocator
{
175 async
fn write_struct
<E
, T
>(output
: &mut T
, data
: E
) -> io
::Result
<()>
177 T
: AsyncWrite
+ ?Sized
+ Unpin
,
180 let data
= data
.to_le();
183 std
::slice
::from_raw_parts(
184 &data
as *const E
as *const u8,
185 core
::mem
::size_of_val(&data
),
188 output
.write_all(data
).await
191 /// Represents an Entry in a ZIP File
193 /// used to add to a ZipEncoder
194 pub struct ZipEntry
{
199 uncompressed_size
: u64,
200 compressed_size
: u64,
206 /// Creates a new ZipEntry
208 /// if is_file is false the path will contain an trailing separator,
209 /// so that the zip file understands that it is a directory
210 pub fn new
<P
: AsRef
<Path
>>(path
: P
, mtime
: i64, mode
: u16, is_file
: bool
) -> Self {
211 let mut relpath
= PathBuf
::new();
213 for comp
in path
.as_ref().components() {
214 if let Component
::Normal(_
) = comp
{
220 relpath
.push(""); // adds trailing slash
224 filename
: relpath
.into(),
228 uncompressed_size
: 0,
235 async
fn write_local_header
<W
>(&self, mut buf
: &mut W
) -> io
::Result
<usize>
237 W
: AsyncWrite
+ Unpin
+ ?Sized
,
239 let filename
= self.filename
.as_bytes();
240 let filename_len
= filename
.len();
241 let header_size
= size_of
::<LocalFileHeader
>();
242 let zip_field_size
= size_of
::<Zip64Field
>();
243 let size
: usize = header_size
+ filename_len
+ zip_field_size
;
245 let (date
, time
) = epoch_to_dos(self.mtime
);
250 signature
: LOCAL_FH_SIG
,
251 version_needed
: 0x2d,
257 compressed_size
: 0xFFFFFFFF,
258 uncompressed_size
: 0xFFFFFFFF,
259 filename_len
: filename_len
as u16,
260 extra_field_len
: zip_field_size
as u16,
265 buf
.write_all(filename
).await?
;
272 uncompressed_size
: 0,
281 async
fn write_data_descriptor
<W
: AsyncWrite
+ Unpin
+ ?Sized
>(
284 ) -> io
::Result
<usize> {
285 let size
= size_of
::<LocalFileFooter
>();
290 signature
: LOCAL_FF_SIG
,
292 compressed_size
: self.compressed_size
,
293 uncompressed_size
: self.uncompressed_size
,
301 async
fn write_central_directory_header
<W
: AsyncWrite
+ Unpin
+ ?Sized
>(
304 ) -> io
::Result
<usize> {
305 let filename
= self.filename
.as_bytes();
306 let filename_len
= filename
.len();
307 let header_size
= size_of
::<CentralDirectoryFileHeader
>();
308 let zip_field_size
= size_of
::<Zip64FieldWithOffset
>();
309 let mut size
: usize = header_size
+ filename_len
;
311 let (date
, time
) = epoch_to_dos(self.mtime
);
313 let (compressed_size
, uncompressed_size
, offset
, need_zip64
) = if self.compressed_size
315 || self.uncompressed_size
>= (u32::MAX
as u64)
316 || self.offset
>= (u32::MAX
as u64)
318 size
+= zip_field_size
;
319 (0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, true)
322 self.compressed_size
as u32,
323 self.uncompressed_size
as u32,
331 CentralDirectoryFileHeader
{
332 signature
: CENTRAL_DIRECTORY_FH_SIG
,
333 version_made_by
: VERSION_MADE_BY
,
334 version_needed
: VERSION_NEEDED
,
342 filename_len
: filename_len
as u16,
343 extra_field_len
: if need_zip64 { zip_field_size as u16 }
else { 0 }
,
347 external_flags
: (self.mode
as u32) << 16 | (!self.is_file
as u32) << 4,
353 buf
.write_all(filename
).await?
;
358 Zip64FieldWithOffset
{
360 field_size
: 3 * 8 + 4,
361 uncompressed_size
: self.uncompressed_size
,
362 compressed_size
: self.compressed_size
,
374 // wraps an asyncreader and calculates the hash
375 struct HashWrapper
<R
> {
380 impl<R
> HashWrapper
<R
> {
381 fn new(inner
: R
) -> Self {
384 hasher
: Hasher
::new(),
388 // consumes self and returns the hash and the reader
389 fn finish(self) -> (u32, R
) {
390 let crc32
= self.hasher
.finalize();
395 impl<R
> AsyncRead
for HashWrapper
<R
>
397 R
: AsyncRead
+ Unpin
,
400 self: Pin
<&mut Self>,
401 cx
: &mut Context
<'_
>,
402 buf
: &mut ReadBuf
<'_
>,
403 ) -> Poll
<Result
<(), io
::Error
>> {
404 let this
= self.get_mut();
405 let old_len
= buf
.filled().len();
406 ready
!(Pin
::new(&mut this
.inner
).poll_read(cx
, buf
))?
;
407 let new_len
= buf
.filled().len();
408 if new_len
> old_len
{
409 this
.hasher
.update(&buf
.filled()[old_len
..new_len
]);
415 /// Wraps a writer that implements AsyncWrite for creating a ZIP archive
417 /// This will create a ZIP archive on the fly with files added with
418 /// 'add_entry'. To Finish the file, call 'finish'
421 /// use anyhow::{Error, Result};
422 /// use tokio::fs::File;
424 /// use pbs_tools::zip::{ZipEncoder, ZipEntry};
427 /// async fn main() -> Result<(), Error> {
428 /// let target = File::open("foo.zip").await?;
429 /// let mut source = File::open("foo.txt").await?;
431 /// let mut zip = ZipEncoder::new(target);
432 /// zip.add_entry(ZipEntry::new(
437 /// ), Some(source)).await?;
439 /// zip.finish().await?;
444 pub struct ZipEncoder
<W
>
446 W
: AsyncWrite
+ Unpin
,
449 files
: Vec
<ZipEntry
>,
453 impl<W
: AsyncWrite
+ Unpin
> ZipEncoder
<W
> {
454 pub fn new(target
: W
) -> Self {
458 target
: Some(target
),
462 pub async
fn add_entry
<R
: AsyncRead
+ Unpin
>(
466 ) -> Result
<(), Error
> {
467 let mut target
= self
470 .ok_or_else(|| format_err
!("had no target during add entry"))?
;
471 entry
.offset
= self.byte_count
.try_into()?
;
472 self.byte_count
+= entry
.write_local_header(&mut target
).await?
;
473 if let Some(content
) = content
{
474 let mut reader
= HashWrapper
::new(content
);
475 let mut enc
= DeflateEncoder
::with_quality(target
, Level
::Fastest
);
477 enc
.compress(&mut reader
).await?
;
478 let total_in
= enc
.total_in();
479 let total_out
= enc
.total_out();
480 target
= enc
.into_inner();
482 let (crc32
, _reader
) = reader
.finish();
484 self.byte_count
+= total_out
as usize;
485 entry
.compressed_size
= total_out
;
486 entry
.uncompressed_size
= total_in
;
490 self.byte_count
+= entry
.write_data_descriptor(&mut target
).await?
;
491 self.target
= Some(target
);
493 self.files
.push(entry
);
500 central_dir_size
: usize,
501 central_dir_offset
: usize,
502 ) -> Result
<(), Error
> {
503 let entrycount
= self.files
.len();
504 let mut target
= self
507 .ok_or_else(|| format_err
!("had no target during write_eocd"))?
;
509 let mut count
= entrycount
as u16;
510 let mut directory_size
= central_dir_size
as u32;
511 let mut directory_offset
= central_dir_offset
as u32;
513 if central_dir_size
> u32::MAX
as usize
514 || central_dir_offset
> u32::MAX
as usize
515 || entrycount
> u16::MAX
as usize
518 directory_size
= 0xFFFFFFFF;
519 directory_offset
= 0xFFFFFFFF;
524 signature
: ZIP64_EOCD_RECORD
,
526 version_made_by
: VERSION_MADE_BY
,
527 version_needed
: VERSION_NEEDED
,
529 disk_number_central_dir
: 0,
530 disk_record_count
: entrycount
.try_into()?
,
531 total_record_count
: entrycount
.try_into()?
,
532 directory_size
: central_dir_size
.try_into()?
,
533 directory_offset
: central_dir_offset
.try_into()?
,
538 let locator_offset
= central_dir_offset
+ central_dir_size
;
543 signature
: ZIP64_EOCD_LOCATOR
,
545 offset
: locator_offset
.try_into()?
,
555 signature
: END_OF_CENTRAL_DIR
,
558 disk_record_count
: count
,
559 total_record_count
: count
,
567 self.target
= Some(target
);
572 pub async
fn finish(&mut self) -> Result
<(), Error
> {
573 let mut target
= self
576 .ok_or_else(|| format_err
!("had no target during finish"))?
;
577 let central_dir_offset
= self.byte_count
;
578 let mut central_dir_size
= 0;
580 for file
in &self.files
{
581 central_dir_size
+= file
.write_central_directory_header(&mut target
).await?
;
584 self.target
= Some(target
);
585 self.write_eocd(central_dir_size
, central_dir_offset
)
590 .ok_or_else(|| format_err
!("had no target for flush"))?
598 /// Zip a local directory and write encoded data to target. "source" has to point to a valid
599 /// directory, it's name will be the root of the zip file - e.g.:
607 /// ...except if "source" is the root directory
608 pub async
fn zip_directory
<W
>(target
: W
, source
: &Path
) -> Result
<(), Error
>
610 W
: AsyncWrite
+ Unpin
+ Send
,
612 use walkdir
::WalkDir
;
613 use std
::os
::unix
::fs
::MetadataExt
;
615 let base_path
= source
.parent().unwrap_or_else(|| Path
::new("/"));
616 let mut encoder
= ZipEncoder
::new(target
);
618 for entry
in WalkDir
::new(&source
).into_iter() {
621 let entry_path
= entry
.path().to_owned();
622 let encoder
= &mut encoder
;
624 if let Err(err
) = async
move {
625 let entry_path_no_base
= entry
.path().strip_prefix(base_path
)?
;
626 let metadata
= entry
.metadata()?
;
627 let mtime
= match metadata
.modified().unwrap_or_else(|_
| SystemTime
::now()).duration_since(SystemTime
::UNIX_EPOCH
) {
628 Ok(dur
) => dur
.as_secs() as i64,
629 Err(time_error
) => -(time_error
.duration().as_secs() as i64)
631 let mode
= metadata
.mode() as u16;
633 if entry
.file_type().is_file() {
634 let file
= tokio
::fs
::File
::open(entry
.path()).await?
;
635 let ze
= ZipEntry
::new(
641 encoder
.add_entry(ze
, Some(file
)).await?
;
642 } else if entry
.file_type().is_dir() {
643 let ze
= ZipEntry
::new(
649 let content
: Option
<tokio
::fs
::File
> = None
;
650 encoder
.add_entry(ze
, content
).await?
;
652 // ignore other file types
653 let ok
: Result
<(), Error
> = Ok(());
659 "zip: error encoding file or directory '{}': {}",
660 entry_path
.display(),
666 eprintln
!("zip: error reading directory entry: {}", err
);
671 encoder
.finish().await