]> git.proxmox.com Git - proxmox-backup.git/blob - pbs-client/src/pxar/extract.rs
tree-wide: bump edition to 2021
[proxmox-backup.git] / pbs-client / src / pxar / extract.rs
1 //! Code for extraction of pxar contents onto the file system.
2
3 use std::collections::HashMap;
4 use std::ffi::{CStr, CString, OsStr, OsString};
5 use std::io;
6 use std::os::unix::ffi::OsStrExt;
7 use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
8 use std::path::{Path, PathBuf};
9 use std::sync::{Arc, Mutex};
10
11 use anyhow::{bail, format_err, Error};
12 use nix::dir::Dir;
13 use nix::fcntl::OFlag;
14 use nix::sys::stat::Mode;
15
16 use pathpatterns::{MatchEntry, MatchList, MatchType};
17 use pxar::accessor::aio::{Accessor, FileContents, FileEntry};
18 use pxar::decoder::{aio::Decoder, Contents};
19 use pxar::format::Device;
20 use pxar::{Entry, EntryKind, Metadata};
21
22 use proxmox_io::{sparse_copy, sparse_copy_async};
23 use proxmox_sys::c_result;
24 use proxmox_sys::fs::{create_path, CreateOptions};
25
26 use proxmox_compression::zip::{ZipEncoder, ZipEntry};
27
28 use crate::pxar::dir_stack::PxarDirStack;
29 use crate::pxar::metadata;
30 use crate::pxar::Flags;
31
32 pub struct PxarExtractOptions<'a> {
33 pub match_list: &'a [MatchEntry],
34 pub extract_match_default: bool,
35 pub allow_existing_dirs: bool,
36 pub overwrite: bool,
37 pub on_error: Option<ErrorHandler>,
38 }
39
40 pub type ErrorHandler = Box<dyn FnMut(Error) -> Result<(), Error> + Send>;
41
42 pub fn extract_archive<T, F>(
43 mut decoder: pxar::decoder::Decoder<T>,
44 destination: &Path,
45 feature_flags: Flags,
46 mut callback: F,
47 options: PxarExtractOptions,
48 ) -> Result<(), Error>
49 where
50 T: pxar::decoder::SeqRead,
51 F: FnMut(&Path),
52 {
53 // we use this to keep track of our directory-traversal
54 decoder.enable_goodbye_entries(true);
55
56 let root = decoder
57 .next()
58 .ok_or_else(|| format_err!("found empty pxar archive"))?
59 .map_err(|err| format_err!("error reading pxar archive: {}", err))?;
60
61 if !root.is_dir() {
62 bail!("pxar archive does not start with a directory entry!");
63 }
64
65 create_path(
66 destination,
67 None,
68 Some(CreateOptions::new().perm(Mode::from_bits_truncate(0o700))),
69 )
70 .map_err(|err| format_err!("error creating directory {:?}: {}", destination, err))?;
71
72 let dir = Dir::open(
73 destination,
74 OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
75 Mode::empty(),
76 )
77 .map_err(|err| format_err!("unable to open target directory {:?}: {}", destination, err,))?;
78
79 let mut extractor = Extractor::new(
80 dir,
81 root.metadata().clone(),
82 options.allow_existing_dirs,
83 options.overwrite,
84 feature_flags,
85 );
86
87 if let Some(on_error) = options.on_error {
88 extractor.on_error(on_error);
89 }
90
91 let mut match_stack = Vec::new();
92 let mut err_path_stack = vec![OsString::from("/")];
93 let mut current_match = options.extract_match_default;
94 while let Some(entry) = decoder.next() {
95 let entry = entry.map_err(|err| format_err!("error reading pxar archive: {}", err))?;
96
97 let file_name_os = entry.file_name();
98
99 // safety check: a file entry in an archive must never contain slashes:
100 if file_name_os.as_bytes().contains(&b'/') {
101 bail!("archive file entry contains slashes, which is invalid and a security concern");
102 }
103
104 let file_name = CString::new(file_name_os.as_bytes())
105 .map_err(|_| format_err!("encountered file name with null-bytes"))?;
106
107 let metadata = entry.metadata();
108
109 extractor.set_path(entry.path().as_os_str().to_owned());
110
111 let match_result = options.match_list.matches(
112 entry.path().as_os_str().as_bytes(),
113 Some(metadata.file_type() as u32),
114 );
115
116 let did_match = match match_result {
117 Some(MatchType::Include) => true,
118 Some(MatchType::Exclude) => false,
119 None => current_match,
120 };
121 match (did_match, entry.kind()) {
122 (_, EntryKind::Directory) => {
123 callback(entry.path());
124
125 let create = current_match && match_result != Some(MatchType::Exclude);
126 extractor
127 .enter_directory(file_name_os.to_owned(), metadata.clone(), create)
128 .map_err(|err| format_err!("error at entry {:?}: {}", file_name_os, err))?;
129
130 // We're starting a new directory, push our old matching state and replace it with
131 // our new one:
132 match_stack.push(current_match);
133 current_match = did_match;
134
135 // When we hit the goodbye table we'll try to apply metadata to the directory, but
136 // the Goodbye entry will not contain the path, so push it to our path stack for
137 // error messages:
138 err_path_stack.push(extractor.clone_path());
139
140 Ok(())
141 }
142 (_, EntryKind::GoodbyeTable) => {
143 // go up a directory
144
145 extractor.set_path(err_path_stack.pop().ok_or_else(|| {
146 format_err!(
147 "error at entry {:?}: unexpected end of directory",
148 file_name_os
149 )
150 })?);
151
152 extractor
153 .leave_directory()
154 .map_err(|err| format_err!("error at entry {:?}: {}", file_name_os, err))?;
155
156 // We left a directory, also get back our previous matching state. This is in sync
157 // with `dir_stack` so this should never be empty except for the final goodbye
158 // table, in which case we get back to the default of `true`.
159 current_match = match_stack.pop().unwrap_or(true);
160
161 Ok(())
162 }
163 (true, EntryKind::Symlink(link)) => {
164 callback(entry.path());
165 extractor.extract_symlink(&file_name, metadata, link.as_ref())
166 }
167 (true, EntryKind::Hardlink(link)) => {
168 callback(entry.path());
169 extractor.extract_hardlink(&file_name, link.as_os_str())
170 }
171 (true, EntryKind::Device(dev)) => {
172 if extractor.contains_flags(Flags::WITH_DEVICE_NODES) {
173 callback(entry.path());
174 extractor.extract_device(&file_name, metadata, dev)
175 } else {
176 Ok(())
177 }
178 }
179 (true, EntryKind::Fifo) => {
180 if extractor.contains_flags(Flags::WITH_FIFOS) {
181 callback(entry.path());
182 extractor.extract_special(&file_name, metadata, 0)
183 } else {
184 Ok(())
185 }
186 }
187 (true, EntryKind::Socket) => {
188 if extractor.contains_flags(Flags::WITH_SOCKETS) {
189 callback(entry.path());
190 extractor.extract_special(&file_name, metadata, 0)
191 } else {
192 Ok(())
193 }
194 }
195 (true, EntryKind::File { size, .. }) => extractor.extract_file(
196 &file_name,
197 metadata,
198 *size,
199 &mut decoder.contents().ok_or_else(|| {
200 format_err!("found regular file entry without contents in archive")
201 })?,
202 extractor.overwrite,
203 ),
204 (false, _) => Ok(()), // skip this
205 }
206 .map_err(|err| format_err!("error at entry {:?}: {}", file_name_os, err))?;
207 }
208
209 if !extractor.dir_stack.is_empty() {
210 bail!("unexpected eof while decoding pxar archive");
211 }
212
213 Ok(())
214 }
215
216 /// Common state for file extraction.
217 pub struct Extractor {
218 feature_flags: Flags,
219 allow_existing_dirs: bool,
220 overwrite: bool,
221 dir_stack: PxarDirStack,
222
223 /// For better error output we need to track the current path in the Extractor state.
224 current_path: Arc<Mutex<OsString>>,
225
226 /// Error callback. Includes `current_path` in the reformatted error, should return `Ok` to
227 /// continue extracting or the passed error as `Err` to bail out.
228 on_error: ErrorHandler,
229 }
230
231 impl Extractor {
232 /// Create a new extractor state for a target directory.
233 pub fn new(
234 root_dir: Dir,
235 metadata: Metadata,
236 allow_existing_dirs: bool,
237 overwrite: bool,
238 feature_flags: Flags,
239 ) -> Self {
240 Self {
241 dir_stack: PxarDirStack::new(root_dir, metadata),
242 allow_existing_dirs,
243 overwrite,
244 feature_flags,
245 current_path: Arc::new(Mutex::new(OsString::new())),
246 on_error: Box::new(Err),
247 }
248 }
249
250 /// We call this on errors. The error will be reformatted to include `current_path`. The
251 /// callback should decide whether this error was fatal (simply return it) to bail out early,
252 /// or log/remember/accumulate errors somewhere and return `Ok(())` in its place to continue
253 /// extracting.
254 pub fn on_error(&mut self, mut on_error: Box<dyn FnMut(Error) -> Result<(), Error> + Send>) {
255 let path = Arc::clone(&self.current_path);
256 self.on_error = Box::new(move |err: Error| -> Result<(), Error> {
257 on_error(format_err!("error at {:?}: {}", path.lock().unwrap(), err))
258 });
259 }
260
261 pub fn set_path(&mut self, path: OsString) {
262 *self.current_path.lock().unwrap() = path;
263 }
264
265 pub fn clone_path(&self) -> OsString {
266 self.current_path.lock().unwrap().clone()
267 }
268
269 /// When encountering a directory during extraction, this is used to keep track of it. If
270 /// `create` is true it is immediately created and its metadata will be updated once we leave
271 /// it. If `create` is false it will only be created if it is going to have any actual content.
272 pub fn enter_directory(
273 &mut self,
274 file_name: OsString,
275 metadata: Metadata,
276 create: bool,
277 ) -> Result<(), Error> {
278 self.dir_stack.push(file_name, metadata)?;
279
280 if create {
281 self.dir_stack.create_last_dir(self.allow_existing_dirs)?;
282 }
283
284 Ok(())
285 }
286
287 /// When done with a directory we can apply its metadata if it has been created.
288 pub fn leave_directory(&mut self) -> Result<(), Error> {
289 let path_info = self.dir_stack.path().to_owned();
290
291 let dir = self
292 .dir_stack
293 .pop()
294 .map_err(|err| format_err!("unexpected end of directory entry: {}", err))?
295 .ok_or_else(|| format_err!("broken pxar archive (directory stack underrun)"))?;
296
297 if let Some(fd) = dir.try_as_borrowed_fd() {
298 metadata::apply(
299 self.feature_flags,
300 dir.metadata(),
301 fd.as_raw_fd(),
302 &path_info,
303 &mut self.on_error,
304 )
305 .map_err(|err| format_err!("failed to apply directory metadata: {}", err))?;
306 }
307
308 Ok(())
309 }
310
311 fn contains_flags(&self, flag: Flags) -> bool {
312 self.feature_flags.contains(flag)
313 }
314
315 fn parent_fd(&mut self) -> Result<RawFd, Error> {
316 self.dir_stack
317 .last_dir_fd(self.allow_existing_dirs)
318 .map(|d| d.as_raw_fd())
319 .map_err(|err| format_err!("failed to get parent directory file descriptor: {}", err))
320 }
321
322 pub fn extract_symlink(
323 &mut self,
324 file_name: &CStr,
325 metadata: &Metadata,
326 link: &OsStr,
327 ) -> Result<(), Error> {
328 let parent = self.parent_fd()?;
329 nix::unistd::symlinkat(link, Some(parent), file_name)?;
330 metadata::apply_at(
331 self.feature_flags,
332 metadata,
333 parent,
334 file_name,
335 self.dir_stack.path(),
336 &mut self.on_error,
337 )
338 }
339
340 pub fn extract_hardlink(&mut self, file_name: &CStr, link: &OsStr) -> Result<(), Error> {
341 crate::pxar::tools::assert_relative_path(link)?;
342
343 let parent = self.parent_fd()?;
344 let root = self.dir_stack.root_dir_fd()?;
345 let target = CString::new(link.as_bytes())?;
346 nix::unistd::linkat(
347 Some(root.as_raw_fd()),
348 target.as_c_str(),
349 Some(parent),
350 file_name,
351 nix::unistd::LinkatFlags::NoSymlinkFollow,
352 )?;
353
354 Ok(())
355 }
356
357 pub fn extract_device(
358 &mut self,
359 file_name: &CStr,
360 metadata: &Metadata,
361 device: &Device,
362 ) -> Result<(), Error> {
363 self.extract_special(file_name, metadata, device.to_dev_t())
364 }
365
366 pub fn extract_special(
367 &mut self,
368 file_name: &CStr,
369 metadata: &Metadata,
370 device: libc::dev_t,
371 ) -> Result<(), Error> {
372 let mode = metadata.stat.mode;
373 let mode = u32::try_from(mode).map_err(|_| {
374 format_err!(
375 "device node's mode contains illegal bits: 0x{:x} (0o{:o})",
376 mode,
377 mode,
378 )
379 })?;
380 let parent = self.parent_fd()?;
381 unsafe { c_result!(libc::mknodat(parent, file_name.as_ptr(), mode, device)) }
382 .map_err(|err| format_err!("failed to create device node: {}", err))?;
383
384 metadata::apply_at(
385 self.feature_flags,
386 metadata,
387 parent,
388 file_name,
389 self.dir_stack.path(),
390 &mut self.on_error,
391 )
392 }
393
394 pub fn extract_file(
395 &mut self,
396 file_name: &CStr,
397 metadata: &Metadata,
398 size: u64,
399 contents: &mut dyn io::Read,
400 overwrite: bool,
401 ) -> Result<(), Error> {
402 let parent = self.parent_fd()?;
403 let mut oflags = OFlag::O_CREAT | OFlag::O_WRONLY | OFlag::O_CLOEXEC;
404 if overwrite {
405 oflags |= OFlag::O_TRUNC;
406 } else {
407 oflags |= OFlag::O_EXCL;
408 }
409 let mut file = unsafe {
410 std::fs::File::from_raw_fd(
411 nix::fcntl::openat(parent, file_name, oflags, Mode::from_bits(0o600).unwrap())
412 .map_err(|err| format_err!("failed to create file {:?}: {}", file_name, err))?,
413 )
414 };
415
416 metadata::apply_initial_flags(
417 self.feature_flags,
418 metadata,
419 file.as_raw_fd(),
420 &mut self.on_error,
421 )
422 .map_err(|err| format_err!("failed to apply initial flags: {}", err))?;
423
424 let result = sparse_copy(&mut *contents, &mut file)
425 .map_err(|err| format_err!("failed to copy file contents: {}", err))?;
426
427 if size != result.written {
428 bail!(
429 "extracted {} bytes of a file of {} bytes",
430 result.written,
431 size
432 );
433 }
434
435 if result.seeked_last {
436 while match nix::unistd::ftruncate(file.as_raw_fd(), size as i64) {
437 Ok(_) => false,
438 Err(errno) if errno == nix::errno::Errno::EINTR => true,
439 Err(err) => bail!("error setting file size: {}", err),
440 } {}
441 }
442
443 metadata::apply(
444 self.feature_flags,
445 metadata,
446 file.as_raw_fd(),
447 self.dir_stack.path(),
448 &mut self.on_error,
449 )
450 }
451
452 pub async fn async_extract_file<T: tokio::io::AsyncRead + Unpin>(
453 &mut self,
454 file_name: &CStr,
455 metadata: &Metadata,
456 size: u64,
457 contents: &mut T,
458 overwrite: bool,
459 ) -> Result<(), Error> {
460 let parent = self.parent_fd()?;
461 let mut oflags = OFlag::O_CREAT | OFlag::O_WRONLY | OFlag::O_CLOEXEC;
462 if overwrite {
463 oflags |= OFlag::O_TRUNC;
464 } else {
465 oflags |= OFlag::O_EXCL;
466 }
467 let mut file = tokio::fs::File::from_std(unsafe {
468 std::fs::File::from_raw_fd(
469 nix::fcntl::openat(parent, file_name, oflags, Mode::from_bits(0o600).unwrap())
470 .map_err(|err| format_err!("failed to create file {:?}: {}", file_name, err))?,
471 )
472 });
473
474 metadata::apply_initial_flags(
475 self.feature_flags,
476 metadata,
477 file.as_raw_fd(),
478 &mut self.on_error,
479 )
480 .map_err(|err| format_err!("failed to apply initial flags: {}", err))?;
481
482 let result = sparse_copy_async(&mut *contents, &mut file)
483 .await
484 .map_err(|err| format_err!("failed to copy file contents: {}", err))?;
485
486 if size != result.written {
487 bail!(
488 "extracted {} bytes of a file of {} bytes",
489 result.written,
490 size
491 );
492 }
493
494 if result.seeked_last {
495 while match nix::unistd::ftruncate(file.as_raw_fd(), size as i64) {
496 Ok(_) => false,
497 Err(errno) if errno == nix::errno::Errno::EINTR => true,
498 Err(err) => bail!("error setting file size: {}", err),
499 } {}
500 }
501
502 metadata::apply(
503 self.feature_flags,
504 metadata,
505 file.as_raw_fd(),
506 self.dir_stack.path(),
507 &mut self.on_error,
508 )
509 }
510 }
511
512 fn add_metadata_to_header(header: &mut tar::Header, metadata: &Metadata) {
513 header.set_mode(metadata.stat.mode as u32);
514 header.set_mtime(metadata.stat.mtime.secs as u64);
515 header.set_uid(metadata.stat.uid as u64);
516 header.set_gid(metadata.stat.gid as u64);
517 }
518
519 async fn tar_add_file<'a, W, T>(
520 tar: &mut proxmox_compression::tar::Builder<W>,
521 contents: Option<Contents<'a, T>>,
522 size: u64,
523 metadata: &Metadata,
524 path: &Path,
525 ) -> Result<(), Error>
526 where
527 T: pxar::decoder::SeqRead + Unpin + Send + Sync + 'static,
528 W: tokio::io::AsyncWrite + Unpin + Send + 'static,
529 {
530 let mut header = tar::Header::new_gnu();
531 header.set_entry_type(tar::EntryType::Regular);
532 header.set_size(size);
533 add_metadata_to_header(&mut header, metadata);
534 header.set_cksum();
535 match contents {
536 Some(content) => tar.add_entry(&mut header, path, content).await,
537 None => tar.add_entry(&mut header, path, tokio::io::empty()).await,
538 }
539 .map_err(|err| format_err!("could not send file entry: {}", err))?;
540 Ok(())
541 }
542
543 /// Creates a tar file from `path` and writes it into `output`
544 pub async fn create_tar<T, W, P>(output: W, accessor: Accessor<T>, path: P) -> Result<(), Error>
545 where
546 T: Clone + pxar::accessor::ReadAt + Unpin + Send + Sync + 'static,
547 W: tokio::io::AsyncWrite + Unpin + Send + 'static,
548 P: AsRef<Path>,
549 {
550 let root = accessor.open_root().await?;
551 let file = root
552 .lookup(&path)
553 .await?
554 .ok_or_else(|| format_err!("error opening '{:?}'", path.as_ref()))?;
555
556 let mut components = file.entry().path().components();
557 components.next_back(); // discard last
558 let prefix = components.as_path();
559
560 let mut tarencoder = proxmox_compression::tar::Builder::new(output);
561 let mut hardlinks: HashMap<PathBuf, PathBuf> = HashMap::new();
562
563 if let Ok(dir) = file.enter_directory().await {
564 let entry = dir.lookup_self().await?;
565 let path = entry.path().strip_prefix(prefix)?;
566
567 if path != Path::new("/") {
568 let metadata = entry.metadata();
569 let mut header = tar::Header::new_gnu();
570 header.set_entry_type(tar::EntryType::Directory);
571 add_metadata_to_header(&mut header, metadata);
572 header.set_size(0);
573 header.set_cksum();
574 tarencoder
575 .add_entry(&mut header, path, tokio::io::empty())
576 .await
577 .map_err(|err| format_err!("could not send dir entry: {}", err))?;
578 }
579
580 let mut decoder = dir.decode_full().await?;
581 decoder.enable_goodbye_entries(false);
582 while let Some(entry) = decoder.next().await {
583 let entry = entry.map_err(|err| format_err!("cannot decode entry: {}", err))?;
584
585 let metadata = entry.metadata();
586 let path = entry.path().strip_prefix(prefix)?;
587
588 match entry.kind() {
589 EntryKind::File { .. } => {
590 let size = decoder.content_size().unwrap_or(0);
591 tar_add_file(&mut tarencoder, decoder.contents(), size, metadata, path).await?
592 }
593 EntryKind::Hardlink(link) => {
594 if !link.data.is_empty() {
595 let entry = root
596 .lookup(&path)
597 .await?
598 .ok_or_else(|| format_err!("error looking up '{:?}'", path))?;
599 let realfile = accessor.follow_hardlink(&entry).await?;
600 let metadata = realfile.entry().metadata();
601 let realpath = Path::new(link);
602
603 log::debug!("adding '{}' to tar", path.display());
604
605 let stripped_path = match realpath.strip_prefix(prefix) {
606 Ok(path) => path,
607 Err(_) => {
608 // outside of our tar archive, add the first occurrence to the tar
609 if let Some(path) = hardlinks.get(realpath) {
610 path
611 } else {
612 let size = decoder.content_size().unwrap_or(0);
613 tar_add_file(
614 &mut tarencoder,
615 decoder.contents(),
616 size,
617 metadata,
618 path,
619 )
620 .await?;
621 hardlinks.insert(realpath.to_owned(), path.to_owned());
622 continue;
623 }
624 }
625 };
626 let mut header = tar::Header::new_gnu();
627 header.set_entry_type(tar::EntryType::Link);
628 add_metadata_to_header(&mut header, metadata);
629 header.set_size(0);
630 tarencoder
631 .add_link(&mut header, path, stripped_path)
632 .await
633 .map_err(|err| format_err!("could not send hardlink entry: {}", err))?;
634 }
635 }
636 EntryKind::Symlink(link) if !link.data.is_empty() => {
637 log::debug!("adding '{}' to tar", path.display());
638 let realpath = Path::new(link);
639 let mut header = tar::Header::new_gnu();
640 header.set_entry_type(tar::EntryType::Symlink);
641 add_metadata_to_header(&mut header, metadata);
642 header.set_size(0);
643 tarencoder
644 .add_link(&mut header, path, realpath)
645 .await
646 .map_err(|err| format_err!("could not send symlink entry: {}", err))?;
647 }
648 EntryKind::Fifo => {
649 log::debug!("adding '{}' to tar", path.display());
650 let mut header = tar::Header::new_gnu();
651 header.set_entry_type(tar::EntryType::Fifo);
652 add_metadata_to_header(&mut header, metadata);
653 header.set_size(0);
654 header.set_device_major(0)?;
655 header.set_device_minor(0)?;
656 header.set_cksum();
657 tarencoder
658 .add_entry(&mut header, path, tokio::io::empty())
659 .await
660 .map_err(|err| format_err!("could not send fifo entry: {}", err))?;
661 }
662 EntryKind::Directory => {
663 log::debug!("adding '{}' to tar", path.display());
664 // we cannot add the root path itself
665 if path != Path::new("/") {
666 let mut header = tar::Header::new_gnu();
667 header.set_entry_type(tar::EntryType::Directory);
668 add_metadata_to_header(&mut header, metadata);
669 header.set_size(0);
670 header.set_cksum();
671 tarencoder
672 .add_entry(&mut header, path, tokio::io::empty())
673 .await
674 .map_err(|err| format_err!("could not send dir entry: {}", err))?;
675 }
676 }
677 EntryKind::Device(device) => {
678 log::debug!("adding '{}' to tar", path.display());
679 let entry_type = if metadata.stat.is_chardev() {
680 tar::EntryType::Char
681 } else {
682 tar::EntryType::Block
683 };
684 let mut header = tar::Header::new_gnu();
685 header.set_entry_type(entry_type);
686 header.set_device_major(device.major as u32)?;
687 header.set_device_minor(device.minor as u32)?;
688 add_metadata_to_header(&mut header, metadata);
689 header.set_size(0);
690 tarencoder
691 .add_entry(&mut header, path, tokio::io::empty())
692 .await
693 .map_err(|err| format_err!("could not send device entry: {}", err))?;
694 }
695 _ => {} // ignore all else
696 }
697 }
698 }
699
700 tarencoder.finish().await.map_err(|err| {
701 log::error!("error during finishing of zip: {}", err);
702 err
703 })?;
704 Ok(())
705 }
706
707 pub async fn create_zip<T, W, P>(output: W, accessor: Accessor<T>, path: P) -> Result<(), Error>
708 where
709 T: Clone + pxar::accessor::ReadAt + Unpin + Send + Sync + 'static,
710 W: tokio::io::AsyncWrite + Unpin + Send + 'static,
711 P: AsRef<Path>,
712 {
713 let root = accessor.open_root().await?;
714 let file = root
715 .lookup(&path)
716 .await?
717 .ok_or_else(|| format_err!("error opening '{:?}'", path.as_ref()))?;
718
719 let prefix = {
720 let mut components = file.entry().path().components();
721 components.next_back(); // discar last
722 components.as_path().to_owned()
723 };
724
725 let mut zip = ZipEncoder::new(output);
726
727 if let Ok(dir) = file.enter_directory().await {
728 let entry = dir.lookup_self().await?;
729 let path = entry.path().strip_prefix(&prefix)?;
730 if path != Path::new("/") {
731 let metadata = entry.metadata();
732 let entry = ZipEntry::new(
733 path,
734 metadata.stat.mtime.secs,
735 metadata.stat.mode as u16,
736 false,
737 );
738 zip.add_entry::<FileContents<T>>(entry, None).await?;
739 }
740
741 let mut decoder = dir.decode_full().await?;
742 decoder.enable_goodbye_entries(false);
743 while let Some(entry) = decoder.next().await {
744 let entry = entry?;
745 let metadata = entry.metadata();
746 let path = entry.path().strip_prefix(&prefix)?;
747
748 match entry.kind() {
749 EntryKind::File { .. } => {
750 log::debug!("adding '{}' to zip", path.display());
751 let entry = ZipEntry::new(
752 path,
753 metadata.stat.mtime.secs,
754 metadata.stat.mode as u16,
755 true,
756 );
757 zip.add_entry(entry, decoder.contents())
758 .await
759 .map_err(|err| format_err!("could not send file entry: {}", err))?;
760 }
761 EntryKind::Hardlink(_) => {
762 let entry = root
763 .lookup(&path)
764 .await?
765 .ok_or_else(|| format_err!("error looking up '{:?}'", path))?;
766 let realfile = accessor.follow_hardlink(&entry).await?;
767 let metadata = realfile.entry().metadata();
768 log::debug!("adding '{}' to zip", path.display());
769 let entry = ZipEntry::new(
770 path,
771 metadata.stat.mtime.secs,
772 metadata.stat.mode as u16,
773 true,
774 );
775 zip.add_entry(entry, decoder.contents())
776 .await
777 .map_err(|err| format_err!("could not send file entry: {}", err))?;
778 }
779 EntryKind::Directory => {
780 log::debug!("adding '{}' to zip", path.display());
781 let entry = ZipEntry::new(
782 path,
783 metadata.stat.mtime.secs,
784 metadata.stat.mode as u16,
785 false,
786 );
787 zip.add_entry::<FileContents<T>>(entry, None).await?;
788 }
789 _ => {} // ignore all else
790 };
791 }
792 }
793
794 zip.finish().await.map_err(|err| {
795 eprintln!("error during finishing of zip: {}", err);
796 err
797 })
798 }
799
800 fn get_extractor<DEST>(destination: DEST, metadata: Metadata) -> Result<Extractor, Error>
801 where
802 DEST: AsRef<Path>,
803 {
804 create_path(
805 &destination,
806 None,
807 Some(CreateOptions::new().perm(Mode::from_bits_truncate(0o700))),
808 )
809 .map_err(|err| {
810 format_err!(
811 "error creating directory {:?}: {}",
812 destination.as_ref(),
813 err
814 )
815 })?;
816
817 let dir = Dir::open(
818 destination.as_ref(),
819 OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
820 Mode::empty(),
821 )
822 .map_err(|err| {
823 format_err!(
824 "unable to open target directory {:?}: {}",
825 destination.as_ref(),
826 err,
827 )
828 })?;
829
830 Ok(Extractor::new(dir, metadata, false, false, Flags::DEFAULT))
831 }
832
833 pub async fn extract_sub_dir<T, DEST, PATH>(
834 destination: DEST,
835 decoder: Accessor<T>,
836 path: PATH,
837 ) -> Result<(), Error>
838 where
839 T: Clone + pxar::accessor::ReadAt + Unpin + Send + Sync + 'static,
840 DEST: AsRef<Path>,
841 PATH: AsRef<Path>,
842 {
843 let root = decoder.open_root().await?;
844
845 let mut extractor = get_extractor(
846 destination,
847 root.lookup_self().await?.entry().metadata().clone(),
848 )?;
849
850 let file = root
851 .lookup(&path)
852 .await?
853 .ok_or_else(|| format_err!("error opening '{:?}'", path.as_ref()))?;
854
855 recurse_files_extractor(&mut extractor, file).await
856 }
857
858 pub async fn extract_sub_dir_seq<S, DEST>(
859 destination: DEST,
860 mut decoder: Decoder<S>,
861 ) -> Result<(), Error>
862 where
863 S: pxar::decoder::SeqRead + Unpin + Send + 'static,
864 DEST: AsRef<Path>,
865 {
866 decoder.enable_goodbye_entries(true);
867 let root = match decoder.next().await {
868 Some(Ok(root)) => root,
869 Some(Err(err)) => bail!("error getting root entry from pxar: {}", err),
870 None => bail!("cannot extract empty archive"),
871 };
872
873 let mut extractor = get_extractor(destination, root.metadata().clone())?;
874
875 if let Err(err) = seq_files_extractor(&mut extractor, decoder).await {
876 log::error!("error extracting pxar archive: {}", err);
877 }
878
879 Ok(())
880 }
881
882 fn extract_special(
883 extractor: &mut Extractor,
884 entry: &Entry,
885 file_name: &CStr,
886 ) -> Result<(), Error> {
887 let metadata = entry.metadata();
888 match entry.kind() {
889 EntryKind::Symlink(link) => {
890 extractor.extract_symlink(file_name, metadata, link.as_ref())?;
891 }
892 EntryKind::Hardlink(link) => {
893 extractor.extract_hardlink(file_name, link.as_os_str())?;
894 }
895 EntryKind::Device(dev) => {
896 if extractor.contains_flags(Flags::WITH_DEVICE_NODES) {
897 extractor.extract_device(file_name, metadata, dev)?;
898 }
899 }
900 EntryKind::Fifo => {
901 if extractor.contains_flags(Flags::WITH_FIFOS) {
902 extractor.extract_special(file_name, metadata, 0)?;
903 }
904 }
905 EntryKind::Socket => {
906 if extractor.contains_flags(Flags::WITH_SOCKETS) {
907 extractor.extract_special(file_name, metadata, 0)?;
908 }
909 }
910 _ => bail!("extract_special used with unsupported entry kind"),
911 }
912 Ok(())
913 }
914
915 fn get_filename(entry: &Entry) -> Result<(OsString, CString), Error> {
916 let file_name_os = entry.file_name().to_owned();
917
918 // safety check: a file entry in an archive must never contain slashes:
919 if file_name_os.as_bytes().contains(&b'/') {
920 bail!("archive file entry contains slashes, which is invalid and a security concern");
921 }
922
923 let file_name = CString::new(file_name_os.as_bytes())
924 .map_err(|_| format_err!("encountered file name with null-bytes"))?;
925
926 Ok((file_name_os, file_name))
927 }
928
929 async fn recurse_files_extractor<T>(
930 extractor: &mut Extractor,
931 file: FileEntry<T>,
932 ) -> Result<(), Error>
933 where
934 T: Clone + pxar::accessor::ReadAt + Unpin + Send + Sync + 'static,
935 {
936 let entry = file.entry();
937 let metadata = entry.metadata();
938 let (file_name_os, file_name) = get_filename(entry)?;
939
940 log::debug!("extracting: {}", file.path().display());
941
942 match file.kind() {
943 EntryKind::Directory => {
944 extractor
945 .enter_directory(file_name_os.to_owned(), metadata.clone(), true)
946 .map_err(|err| format_err!("error at entry {:?}: {}", file_name_os, err))?;
947
948 let dir = file.enter_directory().await?;
949 let mut seq_decoder = dir.decode_full().await?;
950 seq_decoder.enable_goodbye_entries(true);
951 seq_files_extractor(extractor, seq_decoder).await?;
952 extractor.leave_directory()?;
953 }
954 EntryKind::File { size, .. } => {
955 extractor
956 .async_extract_file(
957 &file_name,
958 metadata,
959 *size,
960 &mut file.contents().await.map_err(|_| {
961 format_err!("found regular file entry without contents in archive")
962 })?,
963 extractor.overwrite,
964 )
965 .await?
966 }
967 EntryKind::GoodbyeTable => {} // ignore
968 _ => extract_special(extractor, entry, &file_name)?,
969 }
970 Ok(())
971 }
972
973 async fn seq_files_extractor<T>(
974 extractor: &mut Extractor,
975 mut decoder: pxar::decoder::aio::Decoder<T>,
976 ) -> Result<(), Error>
977 where
978 T: pxar::decoder::SeqRead,
979 {
980 let mut dir_level = 0;
981 loop {
982 let entry = match decoder.next().await {
983 Some(entry) => entry?,
984 None => return Ok(()),
985 };
986
987 let metadata = entry.metadata();
988 let (file_name_os, file_name) = get_filename(&entry)?;
989
990 if !matches!(entry.kind(), EntryKind::GoodbyeTable) {
991 log::debug!("extracting: {}", entry.path().display());
992 }
993
994 if let Err(err) = async {
995 match entry.kind() {
996 EntryKind::Directory => {
997 dir_level += 1;
998 extractor
999 .enter_directory(file_name_os.to_owned(), metadata.clone(), true)
1000 .map_err(|err| format_err!("error at entry {:?}: {}", file_name_os, err))?;
1001 }
1002 EntryKind::File { size, .. } => {
1003 extractor
1004 .async_extract_file(
1005 &file_name,
1006 metadata,
1007 *size,
1008 &mut decoder.contents().ok_or_else(|| {
1009 format_err!("found regular file entry without contents in archive")
1010 })?,
1011 extractor.overwrite,
1012 )
1013 .await?
1014 }
1015 EntryKind::GoodbyeTable => {
1016 dir_level -= 1;
1017 extractor.leave_directory()?;
1018 }
1019 _ => extract_special(extractor, &entry, &file_name)?,
1020 }
1021 Ok(()) as Result<(), Error>
1022 }
1023 .await
1024 {
1025 let display = entry.path().display().to_string();
1026 log::error!(
1027 "error extracting {}: {}",
1028 if matches!(entry.kind(), EntryKind::GoodbyeTable) {
1029 "<directory>"
1030 } else {
1031 &display
1032 },
1033 err
1034 );
1035 }
1036
1037 if dir_level < 0 {
1038 // we've encountered one Goodbye more then Directory, meaning we've left the dir we
1039 // started in - exit early, otherwise the extractor might panic
1040 return Ok(());
1041 }
1042 }
1043 }