]> git.proxmox.com Git - proxmox-backup.git/blob - src/backup/catalog_shell.rs
split out pbs-runtime module
[proxmox-backup.git] / src / backup / catalog_shell.rs
1 use std::collections::HashMap;
2 use std::ffi::{CStr, CString, OsStr, OsString};
3 use std::future::Future;
4 use std::io::Write;
5 use std::mem;
6 use std::os::unix::ffi::{OsStrExt, OsStringExt};
7 use std::path::{Path, PathBuf};
8 use std::pin::Pin;
9
10 use anyhow::{bail, format_err, Error};
11 use nix::dir::Dir;
12 use nix::fcntl::OFlag;
13 use nix::sys::stat::Mode;
14
15 use pathpatterns::{MatchEntry, MatchList, MatchPattern, MatchType, PatternFlag};
16 use proxmox::api::api;
17 use proxmox::api::cli::{self, CliCommand, CliCommandMap, CliHelper, CommandLineInterface};
18 use proxmox::tools::fs::{create_path, CreateOptions};
19 use pxar::{EntryKind, Metadata};
20
21 use crate::backup::catalog::{self, DirEntryAttribute};
22 use crate::pxar::fuse::{Accessor, FileEntry};
23 use crate::pxar::Flags;
24 use pbs_runtime::block_in_place;
25 use crate::tools::ControlFlow;
26
27 type CatalogReader = crate::backup::CatalogReader<std::fs::File>;
28
29 const MAX_SYMLINK_COUNT: usize = 40;
30
31 static mut SHELL: Option<usize> = None;
32
33 /// This list defines all the shell commands and their properties
34 /// using the api schema
35 pub fn catalog_shell_cli() -> CommandLineInterface {
36 CommandLineInterface::Nested(
37 CliCommandMap::new()
38 .insert("pwd", CliCommand::new(&API_METHOD_PWD_COMMAND))
39 .insert(
40 "cd",
41 CliCommand::new(&API_METHOD_CD_COMMAND)
42 .arg_param(&["path"])
43 .completion_cb("path", complete_path),
44 )
45 .insert(
46 "ls",
47 CliCommand::new(&API_METHOD_LS_COMMAND)
48 .arg_param(&["path"])
49 .completion_cb("path", complete_path),
50 )
51 .insert(
52 "stat",
53 CliCommand::new(&API_METHOD_STAT_COMMAND)
54 .arg_param(&["path"])
55 .completion_cb("path", complete_path),
56 )
57 .insert(
58 "select",
59 CliCommand::new(&API_METHOD_SELECT_COMMAND)
60 .arg_param(&["path"])
61 .completion_cb("path", complete_path),
62 )
63 .insert(
64 "deselect",
65 CliCommand::new(&API_METHOD_DESELECT_COMMAND)
66 .arg_param(&["path"])
67 .completion_cb("path", complete_path),
68 )
69 .insert(
70 "clear-selected",
71 CliCommand::new(&API_METHOD_CLEAR_SELECTED_COMMAND),
72 )
73 .insert(
74 "list-selected",
75 CliCommand::new(&API_METHOD_LIST_SELECTED_COMMAND),
76 )
77 .insert(
78 "restore-selected",
79 CliCommand::new(&API_METHOD_RESTORE_SELECTED_COMMAND)
80 .arg_param(&["target"])
81 .completion_cb("target", crate::tools::complete_file_name),
82 )
83 .insert(
84 "restore",
85 CliCommand::new(&API_METHOD_RESTORE_COMMAND)
86 .arg_param(&["target"])
87 .completion_cb("target", crate::tools::complete_file_name),
88 )
89 .insert(
90 "find",
91 CliCommand::new(&API_METHOD_FIND_COMMAND).arg_param(&["pattern"]),
92 )
93 .insert(
94 "exit",
95 CliCommand::new(&API_METHOD_EXIT),
96 )
97 .insert_help(),
98 )
99 }
100
101 fn complete_path(complete_me: &str, _map: &HashMap<String, String>) -> Vec<String> {
102 let shell: &mut Shell = unsafe { std::mem::transmute(SHELL.unwrap()) };
103 match shell.complete_path(complete_me) {
104 Ok(list) => list,
105 Err(err) => {
106 eprintln!("error during completion: {}", err);
107 Vec::new()
108 }
109 }
110 }
111
112 // just an empty wrapper so that it is displayed in help/docs, we check
113 // in the readloop for 'exit' again break
114 #[api(input: { properties: {} })]
115 /// Exit the shell
116 async fn exit() -> Result<(), Error> {
117 Ok(())
118 }
119
120 #[api(input: { properties: {} })]
121 /// List the current working directory.
122 async fn pwd_command() -> Result<(), Error> {
123 Shell::with(move |shell| shell.pwd()).await
124 }
125
126 #[api(
127 input: {
128 properties: {
129 path: {
130 type: String,
131 optional: true,
132 description: "target path."
133 }
134 }
135 }
136 )]
137 /// Change the current working directory to the new directory
138 async fn cd_command(path: Option<String>) -> Result<(), Error> {
139 let path = path.as_ref().map(Path::new);
140 Shell::with(move |shell| shell.cd(path)).await
141 }
142
143 #[api(
144 input: {
145 properties: {
146 path: {
147 type: String,
148 optional: true,
149 description: "target path."
150 }
151 }
152 }
153 )]
154 /// List the content of working directory or given path.
155 async fn ls_command(path: Option<String>) -> Result<(), Error> {
156 let path = path.as_ref().map(Path::new);
157 Shell::with(move |shell| shell.ls(path)).await
158 }
159
160 #[api(
161 input: {
162 properties: {
163 path: {
164 type: String,
165 description: "target path."
166 }
167 }
168 }
169 )]
170 /// Read the metadata for a given directory entry.
171 ///
172 /// This is expensive because the data has to be read from the pxar archive, which means reading
173 /// over the network.
174 async fn stat_command(path: String) -> Result<(), Error> {
175 Shell::with(move |shell| shell.stat(PathBuf::from(path))).await
176 }
177
178 #[api(
179 input: {
180 properties: {
181 path: {
182 type: String,
183 description: "target path."
184 }
185 }
186 }
187 )]
188 /// Select an entry for restore.
189 ///
190 /// This will return an error if the entry is already present in the list or
191 /// if an invalid path was provided.
192 async fn select_command(path: String) -> Result<(), Error> {
193 Shell::with(move |shell| shell.select(PathBuf::from(path))).await
194 }
195
196 #[api(
197 input: {
198 properties: {
199 path: {
200 type: String,
201 description: "path to entry to remove from list."
202 }
203 }
204 }
205 )]
206 /// Deselect an entry for restore.
207 ///
208 /// This will return an error if the entry was not found in the list of entries
209 /// selected for restore.
210 async fn deselect_command(path: String) -> Result<(), Error> {
211 Shell::with(move |shell| shell.deselect(PathBuf::from(path))).await
212 }
213
214 #[api( input: { properties: { } })]
215 /// Clear the list of files selected for restore.
216 async fn clear_selected_command() -> Result<(), Error> {
217 Shell::with(move |shell| shell.deselect_all()).await
218 }
219
220 #[api(
221 input: {
222 properties: {
223 patterns: {
224 type: Boolean,
225 description: "List match patterns instead of the matching files.",
226 optional: true,
227 default: false,
228 }
229 }
230 }
231 )]
232 /// List entries currently selected for restore.
233 async fn list_selected_command(patterns: bool) -> Result<(), Error> {
234 Shell::with(move |shell| shell.list_selected(patterns)).await
235 }
236
237 #[api(
238 input: {
239 properties: {
240 pattern: {
241 type: String,
242 description: "Match pattern for matching files in the catalog."
243 },
244 select: {
245 type: bool,
246 optional: true,
247 default: false,
248 description: "Add matching filenames to list for restore."
249 }
250 }
251 }
252 )]
253 /// Find entries in the catalog matching the given match pattern.
254 async fn find_command(pattern: String, select: bool) -> Result<(), Error> {
255 Shell::with(move |shell| shell.find(pattern, select)).await
256 }
257
258 #[api(
259 input: {
260 properties: {
261 target: {
262 type: String,
263 description: "target path for restore on local filesystem."
264 }
265 }
266 }
267 )]
268 /// Restore the selected entries to the given target path.
269 ///
270 /// Target must not exist on the clients filesystem.
271 async fn restore_selected_command(target: String) -> Result<(), Error> {
272 Shell::with(move |shell| shell.restore_selected(PathBuf::from(target))).await
273 }
274
275 #[api(
276 input: {
277 properties: {
278 target: {
279 type: String,
280 description: "target path for restore on local filesystem."
281 },
282 pattern: {
283 type: String,
284 optional: true,
285 description: "match pattern to limit files for restore."
286 }
287 }
288 }
289 )]
290 /// Restore the sub-archive given by the current working directory to target.
291 ///
292 /// By further providing a pattern, the restore can be limited to a narrower
293 /// subset of this sub-archive.
294 /// If pattern is not present or empty, the full archive is restored to target.
295 async fn restore_command(target: String, pattern: Option<String>) -> Result<(), Error> {
296 Shell::with(move |shell| shell.restore(PathBuf::from(target), pattern)).await
297 }
298
299 /// TODO: Should we use this to fix `step()`? Make path resolution behave more like described in
300 /// the path_resolution(7) man page.
301 ///
302 /// The `Path` type's component iterator does not tell us anything about trailing slashes or
303 /// trailing `Component::CurDir` entries. Since we only support regular paths we'll roll our own
304 /// here:
305 enum PathComponent<'a> {
306 Root,
307 CurDir,
308 ParentDir,
309 Normal(&'a OsStr),
310 TrailingSlash,
311 }
312
313 struct PathComponentIter<'a> {
314 path: &'a [u8],
315 state: u8, // 0=beginning, 1=ongoing, 2=trailing, 3=finished (fused)
316 }
317
318 impl std::iter::FusedIterator for PathComponentIter<'_> {}
319
320 impl<'a> Iterator for PathComponentIter<'a> {
321 type Item = PathComponent<'a>;
322
323 fn next(&mut self) -> Option<Self::Item> {
324 if self.path.is_empty() {
325 return None;
326 }
327
328 if self.state == 0 {
329 self.state = 1;
330 if self.path[0] == b'/' {
331 // absolute path
332 self.path = &self.path[1..];
333 return Some(PathComponent::Root);
334 }
335 }
336
337 // skip slashes
338 let had_slashes = self.path[0] == b'/';
339 while self.path.get(0).copied() == Some(b'/') {
340 self.path = &self.path[1..];
341 }
342
343 Some(match self.path {
344 [] if had_slashes => PathComponent::TrailingSlash,
345 [] => return None,
346 [b'.'] | [b'.', b'/', ..] => {
347 self.path = &self.path[1..];
348 PathComponent::CurDir
349 }
350 [b'.', b'.'] | [b'.', b'.', b'/', ..] => {
351 self.path = &self.path[2..];
352 PathComponent::ParentDir
353 }
354 _ => {
355 let end = self
356 .path
357 .iter()
358 .position(|&b| b == b'/')
359 .unwrap_or(self.path.len());
360 let (out, rest) = self.path.split_at(end);
361 self.path = rest;
362 PathComponent::Normal(OsStr::from_bytes(out))
363 }
364 })
365 }
366 }
367
368 pub struct Shell {
369 /// Readline instance handling input and callbacks
370 rl: rustyline::Editor<CliHelper>,
371
372 /// Interactive prompt.
373 prompt: String,
374
375 /// Calalog reader instance to navigate
376 catalog: CatalogReader,
377
378 /// List of selected paths for restore
379 selected: HashMap<OsString, MatchEntry>,
380
381 /// pxar accessor instance for the current pxar archive
382 accessor: Accessor,
383
384 /// The current position in the archive.
385 position: Vec<PathStackEntry>,
386 }
387
388 #[derive(Clone)]
389 struct PathStackEntry {
390 /// This is always available. We mainly navigate through the catalog.
391 catalog: catalog::DirEntry,
392
393 /// Whenever we need something from the actual archive we fill this out. This is cached along
394 /// the entire path.
395 pxar: Option<FileEntry>,
396 }
397
398 impl PathStackEntry {
399 fn new(dir_entry: catalog::DirEntry) -> Self {
400 Self {
401 pxar: None,
402 catalog: dir_entry,
403 }
404 }
405 }
406
407 impl Shell {
408 /// Create a new shell for the given catalog and pxar archive.
409 pub async fn new(
410 mut catalog: CatalogReader,
411 archive_name: &str,
412 archive: Accessor,
413 ) -> Result<Self, Error> {
414 let cli_helper = CliHelper::new(catalog_shell_cli());
415 let mut rl = rustyline::Editor::<CliHelper>::new();
416 rl.set_helper(Some(cli_helper));
417
418 let catalog_root = catalog.root()?;
419 let archive_root = catalog
420 .lookup(&catalog_root, archive_name.as_bytes())?
421 .ok_or_else(|| format_err!("archive not found in catalog"))?;
422 let position = vec![PathStackEntry::new(archive_root)];
423
424 let mut this = Self {
425 rl,
426 prompt: String::new(),
427 catalog,
428 selected: HashMap::new(),
429 accessor: archive,
430 position,
431 };
432 this.update_prompt();
433 Ok(this)
434 }
435
436 async fn with<'a, Fut, R, F>(call: F) -> Result<R, Error>
437 where
438 F: FnOnce(&'a mut Shell) -> Fut,
439 Fut: Future<Output = Result<R, Error>>,
440 F: 'a,
441 Fut: 'a,
442 R: 'static,
443 {
444 let shell: &mut Shell = unsafe { std::mem::transmute(SHELL.unwrap()) };
445 call(&mut *shell).await
446 }
447
448 pub async fn shell(mut self) -> Result<(), Error> {
449 let this = &mut self;
450 unsafe {
451 SHELL = Some(this as *mut Shell as usize);
452 }
453 while let Ok(line) = this.rl.readline(&this.prompt) {
454 if line == "exit" {
455 break;
456 }
457 let helper = this.rl.helper().unwrap();
458 let args = match cli::shellword_split(&line) {
459 Ok(args) => args,
460 Err(err) => {
461 println!("Error: {}", err);
462 continue;
463 }
464 };
465
466 let _ =
467 cli::handle_command_future(helper.cmd_def(), "", args, cli::CliEnvironment::new())
468 .await;
469 this.rl.add_history_entry(line);
470 this.update_prompt();
471 }
472 Ok(())
473 }
474
475 fn update_prompt(&mut self) {
476 self.prompt = "pxar:".to_string();
477 if self.position.len() <= 1 {
478 self.prompt.push('/');
479 } else {
480 for p in self.position.iter().skip(1) {
481 if !p.catalog.name.starts_with(b"/") {
482 self.prompt.push('/');
483 }
484 match std::str::from_utf8(&p.catalog.name) {
485 Ok(entry) => self.prompt.push_str(entry),
486 Err(_) => self.prompt.push_str("<non-utf8-dir>"),
487 }
488 }
489 }
490 self.prompt.push_str(" > ");
491 }
492
493 async fn pwd(&mut self) -> Result<(), Error> {
494 let stack = Self::lookup(
495 &self.position,
496 &mut self.catalog,
497 &self.accessor,
498 None,
499 &mut Some(0),
500 )
501 .await?;
502 let path = Self::format_path_stack(&stack);
503 println!("{:?}", path);
504 Ok(())
505 }
506
507 fn new_path_stack(&self) -> Vec<PathStackEntry> {
508 self.position[..1].to_vec()
509 }
510
511 async fn resolve_symlink(
512 stack: &mut Vec<PathStackEntry>,
513 catalog: &mut CatalogReader,
514 accessor: &Accessor,
515 follow_symlinks: &mut Option<usize>,
516 ) -> Result<(), Error> {
517 if let Some(ref mut symlink_count) = follow_symlinks {
518 *symlink_count += 1;
519 if *symlink_count > MAX_SYMLINK_COUNT {
520 bail!("too many levels of symbolic links");
521 }
522
523 let file = Self::walk_pxar_archive(accessor, &mut stack[..]).await?;
524
525 let path = match file.entry().kind() {
526 EntryKind::Symlink(symlink) => Path::new(symlink.as_os_str()),
527 _ => bail!("symlink in the catalog was not a symlink in the archive"),
528 };
529
530 let new_stack =
531 Self::lookup(&stack, &mut *catalog, accessor, Some(path), follow_symlinks).await?;
532
533 *stack = new_stack;
534
535 Ok(())
536 } else {
537 bail!("target is a symlink");
538 }
539 }
540
541 /// Walk a path and add it to the path stack.
542 ///
543 /// If the symlink count is used, symlinks will be followed, until we hit the cap and error
544 /// out.
545 async fn step(
546 stack: &mut Vec<PathStackEntry>,
547 catalog: &mut CatalogReader,
548 accessor: &Accessor,
549 component: std::path::Component<'_>,
550 follow_symlinks: &mut Option<usize>,
551 ) -> Result<(), Error> {
552 use std::path::Component;
553 match component {
554 Component::Prefix(_) => bail!("invalid path component (prefix)"),
555 Component::RootDir => stack.truncate(1),
556 Component::CurDir => {
557 if stack.last().unwrap().catalog.is_symlink() {
558 Self::resolve_symlink(stack, catalog, accessor, follow_symlinks).await?;
559 }
560 }
561 Component::ParentDir => drop(stack.pop()),
562 Component::Normal(entry) => {
563 if stack.last().unwrap().catalog.is_symlink() {
564 Self::resolve_symlink(stack, catalog, accessor, follow_symlinks).await?;
565 }
566 match catalog.lookup(&stack.last().unwrap().catalog, entry.as_bytes())? {
567 Some(dir) => stack.push(PathStackEntry::new(dir)),
568 None => bail!("no such file or directory: {:?}", entry),
569 }
570 }
571 }
572
573 Ok(())
574 }
575
576 fn step_nofollow(
577 stack: &mut Vec<PathStackEntry>,
578 catalog: &mut CatalogReader,
579 component: std::path::Component<'_>,
580 ) -> Result<(), Error> {
581 use std::path::Component;
582 match component {
583 Component::Prefix(_) => bail!("invalid path component (prefix)"),
584 Component::RootDir => stack.truncate(1),
585 Component::CurDir => {
586 if stack.last().unwrap().catalog.is_symlink() {
587 bail!("target is a symlink");
588 }
589 }
590 Component::ParentDir => drop(stack.pop()),
591 Component::Normal(entry) => {
592 if stack.last().unwrap().catalog.is_symlink() {
593 bail!("target is a symlink");
594 } else {
595 match catalog.lookup(&stack.last().unwrap().catalog, entry.as_bytes())? {
596 Some(dir) => stack.push(PathStackEntry::new(dir)),
597 None => bail!("no such file or directory: {:?}", entry),
598 }
599 }
600 }
601 }
602 Ok(())
603 }
604
605 /// The pxar accessor is required to resolve symbolic links
606 async fn walk_catalog(
607 stack: &mut Vec<PathStackEntry>,
608 catalog: &mut CatalogReader,
609 accessor: &Accessor,
610 path: &Path,
611 follow_symlinks: &mut Option<usize>,
612 ) -> Result<(), Error> {
613 for c in path.components() {
614 Self::step(stack, catalog, accessor, c, follow_symlinks).await?;
615 }
616 Ok(())
617 }
618
619 /// Non-async version cannot follow symlinks.
620 fn walk_catalog_nofollow(
621 stack: &mut Vec<PathStackEntry>,
622 catalog: &mut CatalogReader,
623 path: &Path,
624 ) -> Result<(), Error> {
625 for c in path.components() {
626 Self::step_nofollow(stack, catalog, c)?;
627 }
628 Ok(())
629 }
630
631 /// This assumes that there are no more symlinks in the path stack.
632 async fn walk_pxar_archive(
633 accessor: &Accessor,
634 mut stack: &mut [PathStackEntry],
635 ) -> Result<FileEntry, Error> {
636 if stack[0].pxar.is_none() {
637 stack[0].pxar = Some(accessor.open_root().await?.lookup_self().await?);
638 }
639
640 // Now walk the directory stack:
641 let mut at = 1;
642 while at < stack.len() {
643 if stack[at].pxar.is_some() {
644 at += 1;
645 continue;
646 }
647
648 let parent = stack[at - 1].pxar.as_ref().unwrap();
649 let dir = parent.enter_directory().await?;
650 let name = Path::new(OsStr::from_bytes(&stack[at].catalog.name));
651 stack[at].pxar = Some(
652 dir.lookup(name)
653 .await?
654 .ok_or_else(|| format_err!("no such entry in pxar file: {:?}", name))?,
655 );
656
657 at += 1;
658 }
659
660 Ok(stack.last().unwrap().pxar.clone().unwrap())
661 }
662
663 fn complete_path(&mut self, input: &str) -> Result<Vec<String>, Error> {
664 let mut tmp_stack;
665 let (parent, base, part) = match input.rfind('/') {
666 Some(ind) => {
667 let (base, part) = input.split_at(ind + 1);
668 let path = PathBuf::from(base);
669 if path.is_absolute() {
670 tmp_stack = self.new_path_stack();
671 } else {
672 tmp_stack = self.position.clone();
673 }
674 Self::walk_catalog_nofollow(&mut tmp_stack, &mut self.catalog, &path)?;
675 (&tmp_stack.last().unwrap().catalog, base, part)
676 }
677 None => (&self.position.last().unwrap().catalog, "", input),
678 };
679
680 let entries = self.catalog.read_dir(parent)?;
681
682 let mut out = Vec::new();
683 for entry in entries {
684 let mut name = base.to_string();
685 if entry.name.starts_with(part.as_bytes()) {
686 name.push_str(std::str::from_utf8(&entry.name)?);
687 if entry.is_directory() {
688 name.push('/');
689 }
690 out.push(name);
691 }
692 }
693
694 Ok(out)
695 }
696
697 // Break async recursion here: lookup -> walk_catalog -> step -> lookup
698 fn lookup<'future, 's, 'c, 'a, 'p, 'y>(
699 stack: &'s [PathStackEntry],
700 catalog: &'c mut CatalogReader,
701 accessor: &'a Accessor,
702 path: Option<&'p Path>,
703 follow_symlinks: &'y mut Option<usize>,
704 ) -> Pin<Box<dyn Future<Output = Result<Vec<PathStackEntry>, Error>> + Send + 'future>>
705 where
706 's: 'future,
707 'c: 'future,
708 'a: 'future,
709 'p: 'future,
710 'y: 'future,
711 {
712 Box::pin(async move {
713 Ok(match path {
714 None => stack.to_vec(),
715 Some(path) => {
716 let mut stack = if path.is_absolute() {
717 stack[..1].to_vec()
718 } else {
719 stack.to_vec()
720 };
721 Self::walk_catalog(&mut stack, catalog, accessor, path, follow_symlinks)
722 .await?;
723 stack
724 }
725 })
726 })
727 }
728
729 async fn ls(&mut self, path: Option<&Path>) -> Result<(), Error> {
730 let stack = Self::lookup(
731 &self.position,
732 &mut self.catalog,
733 &self.accessor,
734 path,
735 &mut Some(0),
736 )
737 .await?;
738
739 let last = stack.last().unwrap();
740 if last.catalog.is_directory() {
741 let items = self.catalog.read_dir(&stack.last().unwrap().catalog)?;
742 let mut out = std::io::stdout();
743 // FIXME: columnize
744 for item in items {
745 out.write_all(&item.name)?;
746 out.write_all(b"\n")?;
747 }
748 } else {
749 let mut out = std::io::stdout();
750 out.write_all(&last.catalog.name)?;
751 out.write_all(b"\n")?;
752 }
753 Ok(())
754 }
755
756 async fn stat(&mut self, path: PathBuf) -> Result<(), Error> {
757 let mut stack = Self::lookup(
758 &self.position,
759 &mut self.catalog,
760 &self.accessor,
761 Some(&path),
762 &mut Some(0),
763 )
764 .await?;
765
766 let file = Self::walk_pxar_archive(&self.accessor, &mut stack).await?;
767 std::io::stdout()
768 .write_all(crate::pxar::format_multi_line_entry(file.entry()).as_bytes())?;
769 Ok(())
770 }
771
772 async fn cd(&mut self, path: Option<&Path>) -> Result<(), Error> {
773 match path {
774 Some(path) => {
775 let new_position = Self::lookup(
776 &self.position,
777 &mut self.catalog,
778 &self.accessor,
779 Some(path),
780 &mut None,
781 )
782 .await?;
783 if !new_position.last().unwrap().catalog.is_directory() {
784 bail!("not a directory");
785 }
786 self.position = new_position;
787 }
788 None => self.position.truncate(1),
789 }
790 self.update_prompt();
791 Ok(())
792 }
793
794 /// This stack must have been canonicalized already!
795 fn format_path_stack(stack: &[PathStackEntry]) -> OsString {
796 if stack.len() <= 1 {
797 return OsString::from("/");
798 }
799
800 let mut out = OsString::new();
801 for c in stack.iter().skip(1) {
802 out.push("/");
803 out.push(OsStr::from_bytes(&c.catalog.name));
804 }
805
806 out
807 }
808
809 async fn select(&mut self, path: PathBuf) -> Result<(), Error> {
810 let stack = Self::lookup(
811 &self.position,
812 &mut self.catalog,
813 &self.accessor,
814 Some(&path),
815 &mut Some(0),
816 )
817 .await?;
818
819 let path = Self::format_path_stack(&stack);
820 let entry = MatchEntry::include(MatchPattern::Literal(path.as_bytes().to_vec()));
821 if self.selected.insert(path.clone(), entry).is_some() {
822 println!("path already selected: {:?}", path);
823 } else {
824 println!("added path: {:?}", path);
825 }
826
827 Ok(())
828 }
829
830 async fn deselect(&mut self, path: PathBuf) -> Result<(), Error> {
831 let stack = Self::lookup(
832 &self.position,
833 &mut self.catalog,
834 &self.accessor,
835 Some(&path),
836 &mut Some(0),
837 )
838 .await?;
839
840 let path = Self::format_path_stack(&stack);
841
842 if self.selected.remove(&path).is_some() {
843 println!("removed path from selection: {:?}", path);
844 } else {
845 println!("path not selected: {:?}", path);
846 }
847
848 Ok(())
849 }
850
851 async fn deselect_all(&mut self) -> Result<(), Error> {
852 self.selected.clear();
853 println!("cleared selection");
854 Ok(())
855 }
856
857 async fn list_selected(&mut self, patterns: bool) -> Result<(), Error> {
858 if patterns {
859 self.list_selected_patterns().await
860 } else {
861 self.list_matching_files().await
862 }
863 }
864
865 async fn list_selected_patterns(&self) -> Result<(), Error> {
866 for entry in self.selected.keys() {
867 println!("{:?}", entry);
868 }
869 Ok(())
870 }
871
872 fn build_match_list(&self) -> Vec<MatchEntry> {
873 let mut list = Vec::with_capacity(self.selected.len());
874 for entry in self.selected.values() {
875 list.push(entry.clone());
876 }
877 list
878 }
879
880 async fn list_matching_files(&mut self) -> Result<(), Error> {
881 let matches = self.build_match_list();
882
883 self.catalog.find(
884 &self.position[0].catalog,
885 &mut Vec::new(),
886 &matches,
887 &mut |path: &[u8]| -> Result<(), Error> {
888 let mut out = std::io::stdout();
889 out.write_all(path)?;
890 out.write_all(b"\n")?;
891 Ok(())
892 },
893 )?;
894
895 Ok(())
896 }
897
898 async fn find(&mut self, pattern: String, select: bool) -> Result<(), Error> {
899 let pattern_os = OsString::from(pattern.clone());
900 let pattern_entry =
901 MatchEntry::parse_pattern(pattern, PatternFlag::PATH_NAME, MatchType::Include)?;
902
903 let mut found_some = false;
904 self.catalog.find(
905 &self.position[0].catalog,
906 &mut Vec::new(),
907 &[&pattern_entry],
908 &mut |path: &[u8]| -> Result<(), Error> {
909 found_some = true;
910 let mut out = std::io::stdout();
911 out.write_all(path)?;
912 out.write_all(b"\n")?;
913 Ok(())
914 },
915 )?;
916
917 if found_some && select {
918 self.selected.insert(pattern_os, pattern_entry);
919 }
920
921 Ok(())
922 }
923
924 async fn restore_selected(&mut self, destination: PathBuf) -> Result<(), Error> {
925 if self.selected.is_empty() {
926 bail!("no entries selected");
927 }
928
929 let match_list = self.build_match_list();
930
931 self.restore_with_match_list(destination, &match_list).await
932 }
933
934 async fn restore(
935 &mut self,
936 destination: PathBuf,
937 pattern: Option<String>,
938 ) -> Result<(), Error> {
939 let tmp;
940 let match_list: &[MatchEntry] = match pattern {
941 None => &[],
942 Some(pattern) => {
943 tmp = [MatchEntry::parse_pattern(
944 pattern,
945 PatternFlag::PATH_NAME,
946 MatchType::Include,
947 )?];
948 &tmp
949 }
950 };
951
952 self.restore_with_match_list(destination, match_list).await
953 }
954
955 async fn restore_with_match_list(
956 &mut self,
957 destination: PathBuf,
958 match_list: &[MatchEntry],
959 ) -> Result<(), Error> {
960 create_path(
961 &destination,
962 None,
963 Some(CreateOptions::new().perm(Mode::from_bits_truncate(0o700))),
964 )
965 .map_err(|err| format_err!("error creating directory {:?}: {}", destination, err))?;
966
967 let rootdir = Dir::open(
968 &destination,
969 OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
970 Mode::empty(),
971 )
972 .map_err(|err| {
973 format_err!("unable to open target directory {:?}: {}", destination, err,)
974 })?;
975
976 let mut dir_stack = self.new_path_stack();
977 Self::walk_pxar_archive(&self.accessor, &mut dir_stack).await?;
978 let root_meta = dir_stack
979 .last()
980 .unwrap()
981 .pxar
982 .as_ref()
983 .unwrap()
984 .entry()
985 .metadata()
986 .clone();
987
988 let extractor = crate::pxar::extract::Extractor::new(rootdir, root_meta, true, Flags::DEFAULT);
989
990 let mut extractor = ExtractorState::new(
991 &mut self.catalog,
992 dir_stack,
993 extractor,
994 &match_list,
995 &self.accessor,
996 )?;
997
998 extractor.extract().await
999 }
1000 }
1001
1002 struct ExtractorState<'a> {
1003 path: Vec<u8>,
1004 path_len: usize,
1005 path_len_stack: Vec<usize>,
1006
1007 dir_stack: Vec<PathStackEntry>,
1008
1009 matches: bool,
1010 matches_stack: Vec<bool>,
1011
1012 read_dir: <Vec<catalog::DirEntry> as IntoIterator>::IntoIter,
1013 read_dir_stack: Vec<<Vec<catalog::DirEntry> as IntoIterator>::IntoIter>,
1014
1015 extractor: crate::pxar::extract::Extractor,
1016
1017 catalog: &'a mut CatalogReader,
1018 match_list: &'a [MatchEntry],
1019 accessor: &'a Accessor,
1020 }
1021
1022 impl<'a> ExtractorState<'a> {
1023 pub fn new(
1024 catalog: &'a mut CatalogReader,
1025 dir_stack: Vec<PathStackEntry>,
1026 extractor: crate::pxar::extract::Extractor,
1027 match_list: &'a [MatchEntry],
1028 accessor: &'a Accessor,
1029 ) -> Result<Self, Error> {
1030 let read_dir = catalog
1031 .read_dir(&dir_stack.last().unwrap().catalog)?
1032 .into_iter();
1033 Ok(Self {
1034 path: Vec::new(),
1035 path_len: 0,
1036 path_len_stack: Vec::new(),
1037
1038 dir_stack,
1039
1040 matches: match_list.is_empty(),
1041 matches_stack: Vec::new(),
1042
1043 read_dir,
1044 read_dir_stack: Vec::new(),
1045
1046 extractor,
1047
1048 catalog,
1049 match_list,
1050 accessor,
1051 })
1052 }
1053
1054 pub async fn extract(&mut self) -> Result<(), Error> {
1055 loop {
1056 let entry = match self.read_dir.next() {
1057 Some(entry) => entry,
1058 None => match self.handle_end_of_directory()? {
1059 ControlFlow::Break(()) => break, // done with root directory
1060 ControlFlow::Continue(()) => continue,
1061 },
1062 };
1063
1064 self.path.truncate(self.path_len);
1065 if !entry.name.starts_with(b"/") {
1066 self.path.reserve(entry.name.len() + 1);
1067 self.path.push(b'/');
1068 }
1069 self.path.extend(&entry.name);
1070
1071 self.extractor.set_path(OsString::from_vec(self.path.clone()));
1072 self.handle_entry(entry).await?;
1073 }
1074
1075 Ok(())
1076 }
1077
1078 fn handle_end_of_directory(&mut self) -> Result<ControlFlow<()>, Error> {
1079 // go up a directory:
1080 self.read_dir = match self.read_dir_stack.pop() {
1081 Some(r) => r,
1082 None => return Ok(ControlFlow::Break(())), // out of root directory
1083 };
1084
1085 self.matches = self
1086 .matches_stack
1087 .pop()
1088 .ok_or_else(|| format_err!("internal iterator error (matches_stack)"))?;
1089
1090 self.dir_stack
1091 .pop()
1092 .ok_or_else(|| format_err!("internal iterator error (dir_stack)"))?;
1093
1094 self.path_len = self
1095 .path_len_stack
1096 .pop()
1097 .ok_or_else(|| format_err!("internal iterator error (path_len_stack)"))?;
1098
1099 self.extractor.leave_directory()?;
1100
1101 Ok(ControlFlow::CONTINUE)
1102 }
1103
1104 async fn handle_new_directory(
1105 &mut self,
1106 entry: catalog::DirEntry,
1107 match_result: Option<MatchType>,
1108 ) -> Result<(), Error> {
1109 // enter a new directory:
1110 self.read_dir_stack.push(mem::replace(
1111 &mut self.read_dir,
1112 self.catalog.read_dir(&entry)?.into_iter(),
1113 ));
1114 self.matches_stack.push(self.matches);
1115 self.dir_stack.push(PathStackEntry::new(entry));
1116 self.path_len_stack.push(self.path_len);
1117 self.path_len = self.path.len();
1118
1119 Shell::walk_pxar_archive(&self.accessor, &mut self.dir_stack).await?;
1120 let dir_pxar = self.dir_stack.last().unwrap().pxar.as_ref().unwrap();
1121 let dir_meta = dir_pxar.entry().metadata().clone();
1122 let create = self.matches && match_result != Some(MatchType::Exclude);
1123 self.extractor.enter_directory(dir_pxar.file_name().to_os_string(), dir_meta, create)?;
1124
1125 Ok(())
1126 }
1127
1128 pub async fn handle_entry(&mut self, entry: catalog::DirEntry) -> Result<(), Error> {
1129 let match_result = self.match_list.matches(&self.path, entry.get_file_mode());
1130 let did_match = match match_result {
1131 Some(MatchType::Include) => true,
1132 Some(MatchType::Exclude) => false,
1133 None => self.matches,
1134 };
1135
1136 match (did_match, &entry.attr) {
1137 (_, DirEntryAttribute::Directory { .. }) => {
1138 self.handle_new_directory(entry, match_result).await?;
1139 }
1140 (true, DirEntryAttribute::File { .. }) => {
1141 self.dir_stack.push(PathStackEntry::new(entry));
1142 let file = Shell::walk_pxar_archive(&self.accessor, &mut self.dir_stack).await?;
1143 self.extract_file(file).await?;
1144 self.dir_stack.pop();
1145 }
1146 (true, DirEntryAttribute::Symlink)
1147 | (true, DirEntryAttribute::BlockDevice)
1148 | (true, DirEntryAttribute::CharDevice)
1149 | (true, DirEntryAttribute::Fifo)
1150 | (true, DirEntryAttribute::Socket)
1151 | (true, DirEntryAttribute::Hardlink) => {
1152 let attr = entry.attr.clone();
1153 self.dir_stack.push(PathStackEntry::new(entry));
1154 let file = Shell::walk_pxar_archive(&self.accessor, &mut self.dir_stack).await?;
1155 self.extract_special(file, attr).await?;
1156 self.dir_stack.pop();
1157 }
1158 (false, _) => (), // skip
1159 }
1160
1161 Ok(())
1162 }
1163
1164 fn path(&self) -> &OsStr {
1165 OsStr::from_bytes(&self.path)
1166 }
1167
1168 async fn extract_file(&mut self, entry: FileEntry) -> Result<(), Error> {
1169 match entry.kind() {
1170 pxar::EntryKind::File { size, .. } => {
1171 let file_name = CString::new(entry.file_name().as_bytes())?;
1172 let mut contents = entry.contents().await?;
1173 self.extractor.async_extract_file(
1174 &file_name,
1175 entry.metadata(),
1176 *size,
1177 &mut contents,
1178 )
1179 .await
1180 }
1181 _ => {
1182 bail!(
1183 "catalog file {:?} not a regular file in the archive",
1184 self.path()
1185 );
1186 }
1187 }
1188 }
1189
1190 async fn extract_special(
1191 &mut self,
1192 entry: FileEntry,
1193 catalog_attr: DirEntryAttribute,
1194 ) -> Result<(), Error> {
1195 let file_name = CString::new(entry.file_name().as_bytes())?;
1196 match (catalog_attr, entry.kind()) {
1197 (DirEntryAttribute::Symlink, pxar::EntryKind::Symlink(symlink)) => {
1198 block_in_place(|| self.extractor.extract_symlink(
1199 &file_name,
1200 entry.metadata(),
1201 symlink.as_os_str(),
1202 ))
1203 }
1204 (DirEntryAttribute::Symlink, _) => {
1205 bail!(
1206 "catalog symlink {:?} not a symlink in the archive",
1207 self.path()
1208 );
1209 }
1210
1211 (DirEntryAttribute::Hardlink, pxar::EntryKind::Hardlink(hardlink)) => {
1212 block_in_place(|| self.extractor.extract_hardlink(&file_name, hardlink.as_os_str()))
1213 }
1214 (DirEntryAttribute::Hardlink, _) => {
1215 bail!(
1216 "catalog hardlink {:?} not a hardlink in the archive",
1217 self.path()
1218 );
1219 }
1220
1221 (ref attr, pxar::EntryKind::Device(device)) => {
1222 self.extract_device(attr.clone(), &file_name, device, entry.metadata())
1223 }
1224
1225 (DirEntryAttribute::Fifo, pxar::EntryKind::Fifo) => {
1226 block_in_place(|| self.extractor.extract_special(&file_name, entry.metadata(), 0))
1227 }
1228 (DirEntryAttribute::Fifo, _) => {
1229 bail!("catalog fifo {:?} not a fifo in the archive", self.path());
1230 }
1231
1232 (DirEntryAttribute::Socket, pxar::EntryKind::Socket) => {
1233 block_in_place(|| self.extractor.extract_special(&file_name, entry.metadata(), 0))
1234 }
1235 (DirEntryAttribute::Socket, _) => {
1236 bail!(
1237 "catalog socket {:?} not a socket in the archive",
1238 self.path()
1239 );
1240 }
1241
1242 attr => bail!("unhandled file type {:?} for {:?}", attr, self.path()),
1243 }
1244 }
1245
1246 fn extract_device(
1247 &mut self,
1248 attr: DirEntryAttribute,
1249 file_name: &CStr,
1250 device: &pxar::format::Device,
1251 metadata: &Metadata,
1252 ) -> Result<(), Error> {
1253 match attr {
1254 DirEntryAttribute::BlockDevice => {
1255 if !metadata.stat.is_blockdev() {
1256 bail!(
1257 "catalog block device {:?} is not a block device in the archive",
1258 self.path(),
1259 );
1260 }
1261 }
1262 DirEntryAttribute::CharDevice => {
1263 if !metadata.stat.is_chardev() {
1264 bail!(
1265 "catalog character device {:?} is not a character device in the archive",
1266 self.path(),
1267 );
1268 }
1269 }
1270 _ => {
1271 bail!(
1272 "unexpected file type for {:?} in the catalog, \
1273 which is a device special file in the archive",
1274 self.path(),
1275 );
1276 }
1277 }
1278 block_in_place(|| self.extractor.extract_special(file_name, metadata, device.to_dev_t()))
1279 }
1280 }