]> git.proxmox.com Git - proxmox-backup.git/blame - pbs-tools/src/zip.rs
tfa: handle incompatible challenge data
[proxmox-backup.git] / pbs-tools / src / zip.rs
CommitLineData
fdce52aa
DC
1//! ZIP Helper
2//!
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
6
7use std::convert::TryInto;
8use std::ffi::OsString;
9use std::io;
10use std::mem::size_of;
11use std::os::unix::ffi::OsStrExt;
12use std::path::{Component, Path, PathBuf};
d84e4073
DC
13use std::pin::Pin;
14use std::task::{Context, Poll};
1f03196c 15use std::time::SystemTime;
fdce52aa 16
d84e4073 17use anyhow::{format_err, Error, Result};
fdce52aa 18use endian_trait::Endian;
d84e4073
DC
19use futures::ready;
20use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf};
fdce52aa
DC
21
22use crc32fast::Hasher;
6ef1b649 23use proxmox_time::gmtime;
fdce52aa 24
2b7f8dd5 25use crate::compression::{DeflateEncoder, Level};
d84e4073 26
fdce52aa
DC
27const LOCAL_FH_SIG: u32 = 0x04034B50;
28const LOCAL_FF_SIG: u32 = 0x08074B50;
29const CENTRAL_DIRECTORY_FH_SIG: u32 = 0x02014B50;
30const END_OF_CENTRAL_DIR: u32 = 0x06054B50;
31const VERSION_NEEDED: u16 = 0x002d;
32const VERSION_MADE_BY: u16 = 0x032d;
33
34const ZIP64_EOCD_RECORD: u32 = 0x06064B50;
35const ZIP64_EOCD_LOCATOR: u32 = 0x07064B50;
36
37// bits for time:
38// 0-4: day of the month (1-31)
39// 5-8: month: (1 = jan, etc.)
40// 9-15: year offset from 1980
41//
42// bits for date:
43// 0-4: second / 2
44// 5-10: minute (0-59)
45// 11-15: hour (0-23)
46//
47// see https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-filetimetodosdatetime
48fn epoch_to_dos(epoch: i64) -> (u16, u16) {
49 let gmtime = match gmtime(epoch) {
50 Ok(gmtime) => gmtime,
51 Err(_) => return (0, 0),
52 };
53
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;
58
59 let date: u16 = if gmtime.tm_year > (2108 - 1900) || gmtime.tm_year < (1980 - 1900) {
60 0
61 } else {
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
66 };
67
68 (date, time)
69}
70
71#[derive(Endian)]
72#[repr(C, packed)]
73struct Zip64Field {
74 field_type: u16,
75 field_size: u16,
76 uncompressed_size: u64,
77 compressed_size: u64,
78}
79
80#[derive(Endian)]
81#[repr(C, packed)]
82struct Zip64FieldWithOffset {
83 field_type: u16,
84 field_size: u16,
85 uncompressed_size: u64,
86 compressed_size: u64,
87 offset: u64,
7c166628 88 start_disk: u32,
fdce52aa
DC
89}
90
91#[derive(Endian)]
92#[repr(C, packed)]
93struct LocalFileHeader {
94 signature: u32,
95 version_needed: u16,
96 flags: u16,
97 compression: u16,
98 time: u16,
99 date: u16,
100 crc32: u32,
101 compressed_size: u32,
102 uncompressed_size: u32,
103 filename_len: u16,
104 extra_field_len: u16,
105}
106
107#[derive(Endian)]
108#[repr(C, packed)]
109struct LocalFileFooter {
110 signature: u32,
111 crc32: u32,
112 compressed_size: u64,
113 uncompressed_size: u64,
114}
115
116#[derive(Endian)]
117#[repr(C, packed)]
118struct CentralDirectoryFileHeader {
119 signature: u32,
120 version_made_by: u16,
121 version_needed: u16,
122 flags: u16,
123 compression: u16,
124 time: u16,
125 date: u16,
126 crc32: u32,
127 compressed_size: u32,
128 uncompressed_size: u32,
129 filename_len: u16,
130 extra_field_len: u16,
131 comment_len: u16,
132 start_disk: u16,
133 internal_flags: u16,
134 external_flags: u32,
135 offset: u32,
136}
137
138#[derive(Endian)]
139#[repr(C, packed)]
140struct EndOfCentralDir {
141 signature: u32,
142 disk_number: u16,
143 start_disk: u16,
144 disk_record_count: u16,
145 total_record_count: u16,
146 directory_size: u32,
147 directory_offset: u32,
148 comment_len: u16,
149}
150
151#[derive(Endian)]
152#[repr(C, packed)]
153struct Zip64EOCDRecord {
154 signature: u32,
155 field_size: u64,
156 version_made_by: u16,
157 version_needed: u16,
158 disk_number: u32,
159 disk_number_central_dir: u32,
160 disk_record_count: u64,
161 total_record_count: u64,
162 directory_size: u64,
163 directory_offset: u64,
164}
165
166#[derive(Endian)]
167#[repr(C, packed)]
168struct Zip64EOCDLocator {
169 signature: u32,
170 disk_number: u32,
171 offset: u64,
172 disk_count: u32,
173}
174
175async fn write_struct<E, T>(output: &mut T, data: E) -> io::Result<()>
176where
177 T: AsyncWrite + ?Sized + Unpin,
178 E: Endian,
179{
180 let data = data.to_le();
181
182 let data = unsafe {
183 std::slice::from_raw_parts(
184 &data as *const E as *const u8,
185 core::mem::size_of_val(&data),
186 )
187 };
188 output.write_all(data).await
189}
190
191/// Represents an Entry in a ZIP File
192///
193/// used to add to a ZipEncoder
194pub struct ZipEntry {
195 filename: OsString,
196 mtime: i64,
197 mode: u16,
198 crc32: u32,
199 uncompressed_size: u64,
200 compressed_size: u64,
201 offset: u64,
202 is_file: bool,
203}
204
205impl ZipEntry {
206 /// Creates a new ZipEntry
207 ///
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();
212
213 for comp in path.as_ref().components() {
214 if let Component::Normal(_) = comp {
215 relpath.push(comp);
216 }
217 }
218
219 if !is_file {
220 relpath.push(""); // adds trailing slash
221 }
222
223 Self {
224 filename: relpath.into(),
225 crc32: 0,
226 mtime,
227 mode,
228 uncompressed_size: 0,
229 compressed_size: 0,
230 offset: 0,
231 is_file,
232 }
233 }
234
235 async fn write_local_header<W>(&self, mut buf: &mut W) -> io::Result<usize>
236 where
237 W: AsyncWrite + Unpin + ?Sized,
238 {
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;
244
245 let (date, time) = epoch_to_dos(self.mtime);
246
247 write_struct(
248 &mut buf,
249 LocalFileHeader {
250 signature: LOCAL_FH_SIG,
251 version_needed: 0x2d,
252 flags: 1 << 3,
d84e4073 253 compression: 0x8,
fdce52aa
DC
254 time,
255 date,
256 crc32: 0,
257 compressed_size: 0xFFFFFFFF,
258 uncompressed_size: 0xFFFFFFFF,
259 filename_len: filename_len as u16,
260 extra_field_len: zip_field_size as u16,
261 },
262 )
263 .await?;
264
265 buf.write_all(filename).await?;
266
267 write_struct(
268 &mut buf,
269 Zip64Field {
270 field_type: 0x0001,
271 field_size: 2 * 8,
272 uncompressed_size: 0,
273 compressed_size: 0,
274 },
275 )
276 .await?;
277
278 Ok(size)
279 }
280
281 async fn write_data_descriptor<W: AsyncWrite + Unpin + ?Sized>(
282 &self,
283 mut buf: &mut W,
284 ) -> io::Result<usize> {
285 let size = size_of::<LocalFileFooter>();
286
287 write_struct(
288 &mut buf,
289 LocalFileFooter {
290 signature: LOCAL_FF_SIG,
291 crc32: self.crc32,
292 compressed_size: self.compressed_size,
293 uncompressed_size: self.uncompressed_size,
294 },
295 )
296 .await?;
297
298 Ok(size)
299 }
300
301 async fn write_central_directory_header<W: AsyncWrite + Unpin + ?Sized>(
302 &self,
303 mut buf: &mut W,
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>();
7914e62b 309 let mut size: usize = header_size + filename_len;
fdce52aa
DC
310
311 let (date, time) = epoch_to_dos(self.mtime);
312
7914e62b
DC
313 let (compressed_size, uncompressed_size, offset, need_zip64) = if self.compressed_size
314 >= (u32::MAX as u64)
315 || self.uncompressed_size >= (u32::MAX as u64)
316 || self.offset >= (u32::MAX as u64)
317 {
318 size += zip_field_size;
319 (0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, true)
320 } else {
321 (
322 self.compressed_size as u32,
323 self.uncompressed_size as u32,
324 self.offset as u32,
325 false,
326 )
327 };
328
fdce52aa
DC
329 write_struct(
330 &mut buf,
331 CentralDirectoryFileHeader {
332 signature: CENTRAL_DIRECTORY_FH_SIG,
333 version_made_by: VERSION_MADE_BY,
334 version_needed: VERSION_NEEDED,
335 flags: 1 << 3,
d84e4073 336 compression: 0x8,
fdce52aa
DC
337 time,
338 date,
339 crc32: self.crc32,
7914e62b
DC
340 compressed_size,
341 uncompressed_size,
fdce52aa 342 filename_len: filename_len as u16,
7914e62b 343 extra_field_len: if need_zip64 { zip_field_size as u16 } else { 0 },
fdce52aa
DC
344 comment_len: 0,
345 start_disk: 0,
346 internal_flags: 0,
347 external_flags: (self.mode as u32) << 16 | (!self.is_file as u32) << 4,
7914e62b 348 offset,
fdce52aa
DC
349 },
350 )
351 .await?;
352
353 buf.write_all(filename).await?;
354
7914e62b
DC
355 if need_zip64 {
356 write_struct(
357 &mut buf,
358 Zip64FieldWithOffset {
359 field_type: 1,
360 field_size: 3 * 8 + 4,
361 uncompressed_size: self.uncompressed_size,
362 compressed_size: self.compressed_size,
363 offset: self.offset,
364 start_disk: 0,
365 },
366 )
367 .await?;
368 }
fdce52aa
DC
369
370 Ok(size)
371 }
372}
373
d84e4073
DC
374// wraps an asyncreader and calculates the hash
375struct HashWrapper<R> {
376 inner: R,
377 hasher: Hasher,
378}
379
380impl<R> HashWrapper<R> {
381 fn new(inner: R) -> Self {
382 Self {
383 inner,
384 hasher: Hasher::new(),
385 }
386 }
387
388 // consumes self and returns the hash and the reader
389 fn finish(self) -> (u32, R) {
390 let crc32 = self.hasher.finalize();
391 (crc32, self.inner)
392 }
393}
394
395impl<R> AsyncRead for HashWrapper<R>
396where
397 R: AsyncRead + Unpin,
398{
399 fn poll_read(
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]);
410 }
411 Poll::Ready(Ok(()))
412 }
413}
414
fdce52aa
DC
415/// Wraps a writer that implements AsyncWrite for creating a ZIP archive
416///
417/// This will create a ZIP archive on the fly with files added with
418/// 'add_entry'. To Finish the file, call 'finish'
419/// Example:
420/// ```no_run
dc2876f6 421/// use anyhow::{Error, Result};
a3399f43
WB
422/// use tokio::fs::File;
423///
424/// use pbs_tools::zip::{ZipEncoder, ZipEntry};
fdce52aa 425///
dc2876f6
DC
426/// #[tokio::main]
427/// async fn main() -> Result<(), Error> {
fdce52aa
DC
428/// let target = File::open("foo.zip").await?;
429/// let mut source = File::open("foo.txt").await?;
430///
431/// let mut zip = ZipEncoder::new(target);
dc2876f6 432/// zip.add_entry(ZipEntry::new(
fdce52aa
DC
433/// "foo.txt",
434/// 0,
435/// 0o100755,
436/// true,
dc2876f6 437/// ), Some(source)).await?;
fdce52aa 438///
dc2876f6 439/// zip.finish().await?;
fdce52aa
DC
440///
441/// Ok(())
442/// }
443/// ```
444pub struct ZipEncoder<W>
445where
446 W: AsyncWrite + Unpin,
447{
448 byte_count: usize,
449 files: Vec<ZipEntry>,
d84e4073 450 target: Option<W>,
fdce52aa
DC
451}
452
453impl<W: AsyncWrite + Unpin> ZipEncoder<W> {
454 pub fn new(target: W) -> Self {
455 Self {
456 byte_count: 0,
457 files: Vec::new(),
d84e4073 458 target: Some(target),
fdce52aa
DC
459 }
460 }
461
462 pub async fn add_entry<R: AsyncRead + Unpin>(
463 &mut self,
464 mut entry: ZipEntry,
465 content: Option<R>,
466 ) -> Result<(), Error> {
d84e4073
DC
467 let mut target = self
468 .target
469 .take()
470 .ok_or_else(|| format_err!("had no target during add entry"))?;
fdce52aa 471 entry.offset = self.byte_count.try_into()?;
d84e4073
DC
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);
fdce52aa 476
d84e4073
DC
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();
481
482 let (crc32, _reader) = reader.finish();
483
484 self.byte_count += total_out as usize;
485 entry.compressed_size = total_out;
486 entry.uncompressed_size = total_in;
487
488 entry.crc32 = crc32;
fdce52aa 489 }
d84e4073
DC
490 self.byte_count += entry.write_data_descriptor(&mut target).await?;
491 self.target = Some(target);
fdce52aa
DC
492
493 self.files.push(entry);
494
495 Ok(())
496 }
497
498 async fn write_eocd(
499 &mut self,
500 central_dir_size: usize,
501 central_dir_offset: usize,
502 ) -> Result<(), Error> {
503 let entrycount = self.files.len();
d84e4073
DC
504 let mut target = self
505 .target
506 .take()
507 .ok_or_else(|| format_err!("had no target during write_eocd"))?;
fdce52aa
DC
508
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;
512
513 if central_dir_size > u32::MAX as usize
514 || central_dir_offset > u32::MAX as usize
515 || entrycount > u16::MAX as usize
516 {
517 count = 0xFFFF;
518 directory_size = 0xFFFFFFFF;
519 directory_offset = 0xFFFFFFFF;
520
521 write_struct(
d84e4073 522 &mut target,
fdce52aa
DC
523 Zip64EOCDRecord {
524 signature: ZIP64_EOCD_RECORD,
525 field_size: 44,
526 version_made_by: VERSION_MADE_BY,
527 version_needed: VERSION_NEEDED,
528 disk_number: 0,
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()?,
534 },
535 )
536 .await?;
537
538 let locator_offset = central_dir_offset + central_dir_size;
539
540 write_struct(
d84e4073 541 &mut target,
fdce52aa
DC
542 Zip64EOCDLocator {
543 signature: ZIP64_EOCD_LOCATOR,
544 disk_number: 0,
545 offset: locator_offset.try_into()?,
546 disk_count: 1,
547 },
548 )
549 .await?;
550 }
551
552 write_struct(
d84e4073 553 &mut target,
fdce52aa
DC
554 EndOfCentralDir {
555 signature: END_OF_CENTRAL_DIR,
556 disk_number: 0,
557 start_disk: 0,
558 disk_record_count: count,
559 total_record_count: count,
560 directory_size,
561 directory_offset,
562 comment_len: 0,
563 },
564 )
565 .await?;
566
d84e4073
DC
567 self.target = Some(target);
568
fdce52aa
DC
569 Ok(())
570 }
571
572 pub async fn finish(&mut self) -> Result<(), Error> {
d84e4073
DC
573 let mut target = self
574 .target
575 .take()
576 .ok_or_else(|| format_err!("had no target during finish"))?;
fdce52aa
DC
577 let central_dir_offset = self.byte_count;
578 let mut central_dir_size = 0;
579
580 for file in &self.files {
d84e4073 581 central_dir_size += file.write_central_directory_header(&mut target).await?;
fdce52aa
DC
582 }
583
d84e4073 584 self.target = Some(target);
fdce52aa
DC
585 self.write_eocd(central_dir_size, central_dir_offset)
586 .await?;
587
d84e4073
DC
588 self.target
589 .take()
590 .ok_or_else(|| format_err!("had no target for flush"))?
591 .flush()
592 .await?;
fdce52aa
DC
593
594 Ok(())
595 }
596}
1f03196c
SR
597
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.:
600/// source:
601/// /foo/bar
602/// zip file:
603/// /bar/file1
604/// /bar/dir1
605/// /bar/dir1/file2
606/// ...
607/// ...except if "source" is the root directory
608pub async fn zip_directory<W>(target: W, source: &Path) -> Result<(), Error>
609where
610 W: AsyncWrite + Unpin + Send,
611{
612 use walkdir::WalkDir;
613 use std::os::unix::fs::MetadataExt;
614
615 let base_path = source.parent().unwrap_or_else(|| Path::new("/"));
616 let mut encoder = ZipEncoder::new(target);
617
618 for entry in WalkDir::new(&source).into_iter() {
619 match entry {
620 Ok(entry) => {
621 let entry_path = entry.path().to_owned();
622 let encoder = &mut encoder;
623
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)
630 };
631 let mode = metadata.mode() as u16;
632
633 if entry.file_type().is_file() {
634 let file = tokio::fs::File::open(entry.path()).await?;
635 let ze = ZipEntry::new(
636 &entry_path_no_base,
637 mtime,
638 mode,
639 true,
640 );
641 encoder.add_entry(ze, Some(file)).await?;
642 } else if entry.file_type().is_dir() {
643 let ze = ZipEntry::new(
644 &entry_path_no_base,
645 mtime,
646 mode,
647 false,
648 );
649 let content: Option<tokio::fs::File> = None;
650 encoder.add_entry(ze, content).await?;
651 }
652 // ignore other file types
653 let ok: Result<(), Error> = Ok(());
654 ok
655 }
656 .await
657 {
658 eprintln!(
659 "zip: error encoding file or directory '{}': {}",
660 entry_path.display(),
661 err
662 );
663 }
664 }
665 Err(err) => {
666 eprintln!("zip: error reading directory entry: {}", err);
667 }
668 }
669 }
670
671 encoder.finish().await
672}