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