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