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