]> git.proxmox.com Git - proxmox-backup.git/blame - pbs-client/src/pxar/extract.rs
tree-wide: bump edition to 2021
[proxmox-backup.git] / pbs-client / src / pxar / extract.rs
CommitLineData
c443f58b
WB
1//! Code for extraction of pxar contents onto the file system.
2
23af572d 3use std::collections::HashMap;
98c54240 4use std::ffi::{CStr, CString, OsStr, OsString};
c443f58b 5use std::io;
0bb4036f 6use std::os::unix::ffi::OsStrExt;
c443f58b 7use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
2e219481 8use std::path::{Path, PathBuf};
84d3af3a 9use std::sync::{Arc, Mutex};
c443f58b
WB
10
11use anyhow::{bail, format_err, Error};
12use nix::dir::Dir;
13use nix::fcntl::OFlag;
14use nix::sys::stat::Mode;
15
16use pathpatterns::{MatchEntry, MatchList, MatchType};
2e219481 17use pxar::accessor::aio::{Accessor, FileContents, FileEntry};
23af572d 18use pxar::decoder::{aio::Decoder, Contents};
edf09406
SR
19use pxar::format::Device;
20use pxar::{Entry, EntryKind, Metadata};
c443f58b 21
84d3af3a 22use proxmox_io::{sparse_copy, sparse_copy_async};
25877d05
DM
23use proxmox_sys::c_result;
24use proxmox_sys::fs::{create_path, CreateOptions};
c443f58b 25
b066586a 26use proxmox_compression::zip::{ZipEncoder, ZipEntry};
2b7f8dd5 27
c443f58b 28use crate::pxar::dir_stack::PxarDirStack;
c443f58b 29use crate::pxar::metadata;
d9b8e2c7 30use crate::pxar::Flags;
c443f58b 31
72064fd0 32pub struct PxarExtractOptions<'a> {
84d3af3a 33 pub match_list: &'a [MatchEntry],
72064fd0
FG
34 pub extract_match_default: bool,
35 pub allow_existing_dirs: bool,
95e910f1 36 pub overwrite: bool,
72064fd0
FG
37 pub on_error: Option<ErrorHandler>,
38}
39
e97025ab
FG
40pub type ErrorHandler = Box<dyn FnMut(Error) -> Result<(), Error> + Send>;
41
c443f58b
WB
42pub fn extract_archive<T, F>(
43 mut decoder: pxar::decoder::Decoder<T>,
44 destination: &Path,
5444fa94 45 feature_flags: Flags,
c443f58b 46 mut callback: F,
72064fd0 47 options: PxarExtractOptions,
c443f58b
WB
48) -> Result<(), Error>
49where
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(
16f6766a 66 destination,
c443f58b
WB
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
98c54240
WB
79 let mut extractor = Extractor::new(
80 dir,
81 root.metadata().clone(),
72064fd0 82 options.allow_existing_dirs,
95e910f1 83 options.overwrite,
98c54240
WB
84 feature_flags,
85 );
c443f58b 86
72064fd0 87 if let Some(on_error) = options.on_error {
d9b8e2c7
WB
88 extractor.on_error(on_error);
89 }
90
c443f58b 91 let mut match_stack = Vec::new();
0f921807 92 let mut err_path_stack = vec![OsString::from("/")];
72064fd0 93 let mut current_match = options.extract_match_default;
c443f58b 94 while let Some(entry) = decoder.next() {
c443f58b
WB
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
d9b8e2c7
WB
109 extractor.set_path(entry.path().as_os_str().to_owned());
110
72064fd0 111 let match_result = options.match_list.matches(
c443f58b
WB
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) => {
98c54240 123 callback(entry.path());
c443f58b 124
98c54240 125 let create = current_match && match_result != Some(MatchType::Exclude);
d9b8e2c7
WB
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))?;
c443f58b
WB
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
d9b8e2c7
WB
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
c443f58b
WB
140 Ok(())
141 }
142 (_, EntryKind::GoodbyeTable) => {
143 // go up a directory
d9b8e2c7
WB
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
98c54240
WB
152 extractor
153 .leave_directory()
154 .map_err(|err| format_err!("error at entry {:?}: {}", file_name_os, err))?;
155
c443f58b
WB
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
98c54240 161 Ok(())
c443f58b
WB
162 }
163 (true, EntryKind::Symlink(link)) => {
98c54240 164 callback(entry.path());
c443f58b
WB
165 extractor.extract_symlink(&file_name, metadata, link.as_ref())
166 }
167 (true, EntryKind::Hardlink(link)) => {
98c54240 168 callback(entry.path());
4264e522 169 extractor.extract_hardlink(&file_name, link.as_os_str())
c443f58b
WB
170 }
171 (true, EntryKind::Device(dev)) => {
5444fa94 172 if extractor.contains_flags(Flags::WITH_DEVICE_NODES) {
98c54240 173 callback(entry.path());
c443f58b
WB
174 extractor.extract_device(&file_name, metadata, dev)
175 } else {
176 Ok(())
177 }
178 }
179 (true, EntryKind::Fifo) => {
5444fa94 180 if extractor.contains_flags(Flags::WITH_FIFOS) {
98c54240 181 callback(entry.path());
c443f58b
WB
182 extractor.extract_special(&file_name, metadata, 0)
183 } else {
184 Ok(())
185 }
186 }
187 (true, EntryKind::Socket) => {
5444fa94 188 if extractor.contains_flags(Flags::WITH_SOCKETS) {
98c54240 189 callback(entry.path());
c443f58b
WB
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 })?,
95e910f1 202 extractor.overwrite,
c443f58b
WB
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
98c54240 216/// Common state for file extraction.
2b7f8dd5 217pub struct Extractor {
98c54240
WB
218 feature_flags: Flags,
219 allow_existing_dirs: bool,
95e910f1 220 overwrite: bool,
98c54240 221 dir_stack: PxarDirStack,
d9b8e2c7
WB
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.
e97025ab 228 on_error: ErrorHandler,
98c54240
WB
229}
230
231impl 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,
95e910f1 237 overwrite: bool,
98c54240
WB
238 feature_flags: Flags,
239 ) -> Self {
240 Self {
241 dir_stack: PxarDirStack::new(root_dir, metadata),
242 allow_existing_dirs,
95e910f1 243 overwrite,
98c54240 244 feature_flags,
d9b8e2c7 245 current_path: Arc::new(Mutex::new(OsString::new())),
22a9189e 246 on_error: Box::new(Err),
98c54240
WB
247 }
248 }
249
d9b8e2c7
WB
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
98c54240
WB
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
d9b8e2c7 287 /// When done with a directory we can apply its metadata if it has been created.
98c54240 288 pub fn leave_directory(&mut self) -> Result<(), Error> {
79e58a90
WB
289 let path_info = self.dir_stack.path().to_owned();
290
98c54240
WB
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
dd519bba 297 if let Some(fd) = dir.try_as_borrowed_fd() {
98c54240
WB
298 metadata::apply(
299 self.feature_flags,
300 dir.metadata(),
dd519bba 301 fd.as_raw_fd(),
79e58a90 302 &path_info,
d9b8e2c7 303 &mut self.on_error,
032cd1b8
WB
304 )
305 .map_err(|err| format_err!("failed to apply directory metadata: {}", err))?;
98c54240
WB
306 }
307
308 Ok(())
c443f58b
WB
309 }
310
98c54240
WB
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> {
032cd1b8
WB
316 self.dir_stack
317 .last_dir_fd(self.allow_existing_dirs)
dd519bba 318 .map(|d| d.as_raw_fd())
032cd1b8 319 .map_err(|err| format_err!("failed to get parent directory file descriptor: {}", err))
c443f58b
WB
320 }
321
4264e522 322 pub fn extract_symlink(
c443f58b
WB
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)?;
d9b8e2c7
WB
330 metadata::apply_at(
331 self.feature_flags,
332 metadata,
333 parent,
334 file_name,
79e58a90 335 self.dir_stack.path(),
d9b8e2c7
WB
336 &mut self.on_error,
337 )
c443f58b
WB
338 }
339
d9b8e2c7 340 pub fn extract_hardlink(&mut self, file_name: &CStr, link: &OsStr) -> Result<(), Error> {
c443f58b
WB
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(
dd519bba 347 Some(root.as_raw_fd()),
c443f58b
WB
348 target.as_c_str(),
349 Some(parent),
350 file_name,
351 nix::unistd::LinkatFlags::NoSymlinkFollow,
352 )?;
353
354 Ok(())
355 }
356
4264e522 357 pub fn extract_device(
c443f58b
WB
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
4264e522 366 pub fn extract_special(
c443f58b
WB
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
d9b8e2c7
WB
384 metadata::apply_at(
385 self.feature_flags,
386 metadata,
387 parent,
388 file_name,
79e58a90 389 self.dir_stack.path(),
d9b8e2c7
WB
390 &mut self.on_error,
391 )
c443f58b
WB
392 }
393
4264e522 394 pub fn extract_file(
c443f58b
WB
395 &mut self,
396 file_name: &CStr,
397 metadata: &Metadata,
398 size: u64,
399 contents: &mut dyn io::Read,
95e910f1 400 overwrite: bool,
c443f58b
WB
401 ) -> Result<(), Error> {
402 let parent = self.parent_fd()?;
95e910f1
MF
403 let mut oflags = OFlag::O_CREAT | OFlag::O_WRONLY | OFlag::O_CLOEXEC;
404 if overwrite {
fe2213c9 405 oflags |= OFlag::O_TRUNC;
95e910f1 406 } else {
fe2213c9 407 oflags |= OFlag::O_EXCL;
95e910f1 408 }
c443f58b 409 let mut file = unsafe {
d9b8e2c7 410 std::fs::File::from_raw_fd(
2a23675d
WB
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))?,
032cd1b8 413 )
c443f58b
WB
414 };
415
d9b8e2c7
WB
416 metadata::apply_initial_flags(
417 self.feature_flags,
418 metadata,
419 file.as_raw_fd(),
420 &mut self.on_error,
befd95a9
WB
421 )
422 .map_err(|err| format_err!("failed to apply initial flags: {}", err))?;
032cd1b8 423
cba167b8 424 let result = sparse_copy(&mut *contents, &mut file)
032cd1b8 425 .map_err(|err| format_err!("failed to copy file contents: {}", err))?;
cba167b8
DC
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,
11ca8343 438 Err(errno) if errno == nix::errno::Errno::EINTR => true,
cba167b8
DC
439 Err(err) => bail!("error setting file size: {}", err),
440 } {}
c443f58b
WB
441 }
442
d9b8e2c7
WB
443 metadata::apply(
444 self.feature_flags,
445 metadata,
446 file.as_raw_fd(),
79e58a90 447 self.dir_stack.path(),
d9b8e2c7
WB
448 &mut self.on_error,
449 )
c443f58b 450 }
4264e522
WB
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,
95e910f1 458 overwrite: bool,
4264e522
WB
459 ) -> Result<(), Error> {
460 let parent = self.parent_fd()?;
95e910f1
MF
461 let mut oflags = OFlag::O_CREAT | OFlag::O_WRONLY | OFlag::O_CLOEXEC;
462 if overwrite {
fe2213c9 463 oflags |= OFlag::O_TRUNC;
95e910f1 464 } else {
fe2213c9 465 oflags |= OFlag::O_EXCL;
95e910f1 466 }
4264e522 467 let mut file = tokio::fs::File::from_std(unsafe {
d9b8e2c7 468 std::fs::File::from_raw_fd(
2a23675d
WB
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))?,
032cd1b8 471 )
4264e522
WB
472 });
473
d9b8e2c7
WB
474 metadata::apply_initial_flags(
475 self.feature_flags,
476 metadata,
477 file.as_raw_fd(),
478 &mut self.on_error,
befd95a9
WB
479 )
480 .map_err(|err| format_err!("failed to apply initial flags: {}", err))?;
032cd1b8 481
cba167b8 482 let result = sparse_copy_async(&mut *contents, &mut file)
032cd1b8
WB
483 .await
484 .map_err(|err| format_err!("failed to copy file contents: {}", err))?;
cba167b8
DC
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,
11ca8343 497 Err(errno) if errno == nix::errno::Errno::EINTR => true,
cba167b8
DC
498 Err(err) => bail!("error setting file size: {}", err),
499 } {}
4264e522
WB
500 }
501
d9b8e2c7
WB
502 metadata::apply(
503 self.feature_flags,
504 metadata,
505 file.as_raw_fd(),
79e58a90 506 self.dir_stack.path(),
d9b8e2c7
WB
507 &mut self.on_error,
508 )
4264e522 509 }
c443f58b 510}
2e219481 511
23af572d
DC
512fn 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
519async 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>
526where
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
23af572d 543/// Creates a tar file from `path` and writes it into `output`
e10fccf5 544pub async fn create_tar<T, W, P>(output: W, accessor: Accessor<T>, path: P) -> Result<(), Error>
23af572d
DC
545where
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?
e1db0670 554 .ok_or_else(|| format_err!("error opening '{:?}'", path.as_ref()))?;
23af572d 555
23af572d
DC
556 let mut components = file.entry().path().components();
557 components.next_back(); // discard last
7546e9c9 558 let prefix = components.as_path();
23af572d
DC
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 {
07a683d2
DC
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
23af572d
DC
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();
e3746a32 586 let path = entry.path().strip_prefix(prefix)?;
23af572d
DC
587
588 match entry.kind() {
589 EntryKind::File { .. } => {
590 let size = decoder.content_size().unwrap_or(0);
c39852ab 591 tar_add_file(&mut tarencoder, decoder.contents(), size, metadata, path).await?
23af572d
DC
592 }
593 EntryKind::Hardlink(link) => {
594 if !link.data.is_empty() {
595 let entry = root
596 .lookup(&path)
597 .await?
e1db0670 598 .ok_or_else(|| format_err!("error looking up '{:?}'", path))?;
23af572d
DC
599 let realfile = accessor.follow_hardlink(&entry).await?;
600 let metadata = realfile.entry().metadata();
0bb4036f 601 let realpath = Path::new(link);
23af572d 602
e10fccf5 603 log::debug!("adding '{}' to tar", path.display());
23af572d 604
7546e9c9 605 let stripped_path = match realpath.strip_prefix(prefix) {
23af572d
DC
606 Ok(path) => path,
607 Err(_) => {
74cad4a8 608 // outside of our tar archive, add the first occurrence to the tar
0bb4036f 609 if let Some(path) = hardlinks.get(realpath) {
23af572d
DC
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,
c39852ab 618 path,
23af572d
DC
619 )
620 .await?;
e3746a32 621 hardlinks.insert(realpath.to_owned(), path.to_owned());
23af572d
DC
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() => {
e10fccf5 637 log::debug!("adding '{}' to tar", path.display());
0bb4036f 638 let realpath = Path::new(link);
23af572d
DC
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 => {
e10fccf5 649 log::debug!("adding '{}' to tar", path.display());
23af572d
DC
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 => {
e10fccf5 663 log::debug!("adding '{}' to tar", path.display());
23af572d
DC
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) => {
e10fccf5 678 log::debug!("adding '{}' to tar", path.display());
23af572d
DC
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| {
e10fccf5 701 log::error!("error during finishing of zip: {}", err);
23af572d
DC
702 err
703 })?;
704 Ok(())
705}
706
e10fccf5 707pub async fn create_zip<T, W, P>(output: W, accessor: Accessor<T>, path: P) -> Result<(), Error>
2e219481
DC
708where
709 T: Clone + pxar::accessor::ReadAt + Unpin + Send + Sync + 'static,
710 W: tokio::io::AsyncWrite + Unpin + Send + 'static,
711 P: AsRef<Path>,
712{
7098f5d8 713 let root = accessor.open_root().await?;
2e219481 714 let file = root
84d3af3a
WB
715 .lookup(&path)
716 .await?
e1db0670 717 .ok_or_else(|| format_err!("error opening '{:?}'", path.as_ref()))?;
2e219481 718
7546e9c9
WB
719 let prefix = {
720 let mut components = file.entry().path().components();
721 components.next_back(); // discar last
722 components.as_path().to_owned()
723 };
2e219481 724
7098f5d8 725 let mut zip = ZipEncoder::new(output);
2e219481 726
7098f5d8
DC
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 }
2e219481 740
7098f5d8
DC
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 { .. } => {
e10fccf5 750 log::debug!("adding '{}' to zip", path.display());
7098f5d8
DC
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))?;
2e219481 760 }
7098f5d8
DC
761 EntryKind::Hardlink(_) => {
762 let entry = root
763 .lookup(&path)
764 .await?
e1db0670 765 .ok_or_else(|| format_err!("error looking up '{:?}'", path))?;
7098f5d8
DC
766 let realfile = accessor.follow_hardlink(&entry).await?;
767 let metadata = realfile.entry().metadata();
e10fccf5 768 log::debug!("adding '{}' to zip", path.display());
7098f5d8
DC
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))?;
2e219481 778 }
7098f5d8 779 EntryKind::Directory => {
e10fccf5 780 log::debug!("adding '{}' to zip", path.display());
7098f5d8
DC
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?;
2e219481 788 }
7098f5d8
DC
789 _ => {} // ignore all else
790 };
791 }
792 }
2e219481 793
7098f5d8
DC
794 zip.finish().await.map_err(|err| {
795 eprintln!("error during finishing of zip: {}", err);
796 err
2e219481
DC
797 })
798}
799
edf09406 800fn get_extractor<DEST>(destination: DEST, metadata: Metadata) -> Result<Extractor, Error>
a42212fc 801where
a42212fc 802 DEST: AsRef<Path>,
a42212fc 803{
a42212fc
DC
804 create_path(
805 &destination,
806 None,
807 Some(CreateOptions::new().perm(Mode::from_bits_truncate(0o700))),
808 )
edf09406
SR
809 .map_err(|err| {
810 format_err!(
811 "error creating directory {:?}: {}",
812 destination.as_ref(),
813 err
814 )
815 })?;
a42212fc
DC
816
817 let dir = Dir::open(
818 destination.as_ref(),
819 OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
820 Mode::empty(),
821 )
edf09406
SR
822 .map_err(|err| {
823 format_err!(
824 "unable to open target directory {:?}: {}",
825 destination.as_ref(),
826 err,
827 )
828 })?;
a42212fc 829
95e910f1 830 Ok(Extractor::new(dir, metadata, false, false, Flags::DEFAULT))
edf09406
SR
831}
832
833pub async fn extract_sub_dir<T, DEST, PATH>(
834 destination: DEST,
835 decoder: Accessor<T>,
836 path: PATH,
edf09406
SR
837) -> Result<(), Error>
838where
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,
a42212fc 847 root.lookup_self().await?.entry().metadata().clone(),
edf09406 848 )?;
a42212fc
DC
849
850 let file = root
edf09406
SR
851 .lookup(&path)
852 .await?
e1db0670 853 .ok_or_else(|| format_err!("error opening '{:?}'", path.as_ref()))?;
a42212fc 854
e10fccf5 855 recurse_files_extractor(&mut extractor, file).await
edf09406
SR
856}
857
858pub async fn extract_sub_dir_seq<S, DEST>(
859 destination: DEST,
860 mut decoder: Decoder<S>,
edf09406
SR
861) -> Result<(), Error>
862where
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
e10fccf5
HL
875 if let Err(err) = seq_files_extractor(&mut extractor, decoder).await {
876 log::error!("error extracting pxar archive: {}", err);
edf09406
SR
877 }
878
879 Ok(())
880}
881
882fn 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(())
a42212fc
DC
913}
914
edf09406
SR
915fn 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
e1db0670
FG
929async fn recurse_files_extractor<T>(
930 extractor: &mut Extractor,
a42212fc 931 file: FileEntry<T>,
edf09406 932) -> Result<(), Error>
a42212fc
DC
933where
934 T: Clone + pxar::accessor::ReadAt + Unpin + Send + Sync + 'static,
935{
edf09406
SR
936 let entry = file.entry();
937 let metadata = entry.metadata();
938 let (file_name_os, file_name) = get_filename(entry)?;
a42212fc 939
e10fccf5 940 log::debug!("extracting: {}", file.path().display());
a42212fc 941
edf09406
SR
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);
e10fccf5 951 seq_files_extractor(extractor, seq_decoder).await?;
edf09406 952 extractor.leave_directory()?;
a42212fc 953 }
edf09406
SR
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 })?,
95e910f1 963 extractor.overwrite,
edf09406
SR
964 )
965 .await?
966 }
967 EntryKind::GoodbyeTable => {} // ignore
968 _ => extract_special(extractor, entry, &file_name)?,
969 }
970 Ok(())
971}
a42212fc 972
e1db0670
FG
973async fn seq_files_extractor<T>(
974 extractor: &mut Extractor,
edf09406 975 mut decoder: pxar::decoder::aio::Decoder<T>,
edf09406
SR
976) -> Result<(), Error>
977where
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 };
a42212fc 986
edf09406
SR
987 let metadata = entry.metadata();
988 let (file_name_os, file_name) = get_filename(&entry)?;
a42212fc 989
e10fccf5
HL
990 if !matches!(entry.kind(), EntryKind::GoodbyeTable) {
991 log::debug!("extracting: {}", entry.path().display());
edf09406
SR
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))?;
a42212fc 1001 }
edf09406
SR
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 })?,
95e910f1 1011 extractor.overwrite,
edf09406
SR
1012 )
1013 .await?
a42212fc 1014 }
edf09406
SR
1015 EntryKind::GoodbyeTable => {
1016 dir_level -= 1;
1017 extractor.leave_directory()?;
a42212fc 1018 }
edf09406 1019 _ => extract_special(extractor, &entry, &file_name)?,
a42212fc 1020 }
edf09406
SR
1021 Ok(()) as Result<(), Error>
1022 }
1023 .await
1024 {
1025 let display = entry.path().display().to_string();
e10fccf5 1026 log::error!(
edf09406
SR
1027 "error extracting {}: {}",
1028 if matches!(entry.kind(), EntryKind::GoodbyeTable) {
1029 "<directory>"
1030 } else {
1031 &display
1032 },
1033 err
1034 );
a42212fc 1035 }
a42212fc 1036
edf09406
SR
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}