1 use std
::collections
::HashMap
;
2 use std
::convert
::TryFrom
;
3 use std
::ffi
::{CStr, CString, OsStr, OsString}
;
4 use std
::future
::Future
;
7 use std
::os
::unix
::ffi
::OsStrExt
;
8 use std
::os
::unix
::io
::{AsRawFd, FromRawFd}
;
9 use std
::path
::{Path, PathBuf}
;
12 use anyhow
::{bail, format_err, Error}
;
14 use nix
::fcntl
::OFlag
;
15 use nix
::sys
::stat
::Mode
;
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}
;
24 use crate::backup
::catalog
::{self, DirEntryAttribute}
;
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
;
32 type CatalogReader
= crate::backup
::CatalogReader
<std
::fs
::File
>;
34 const MAX_SYMLINK_COUNT
: usize = 40;
36 static mut SHELL
: Option
<usize> = None
;
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(
43 .insert("pwd", CliCommand
::new(&API_METHOD_PWD_COMMAND
))
46 CliCommand
::new(&API_METHOD_CD_COMMAND
)
48 .completion_cb("path", complete_path
),
52 CliCommand
::new(&API_METHOD_LS_COMMAND
)
54 .completion_cb("path", complete_path
),
58 CliCommand
::new(&API_METHOD_STAT_COMMAND
)
60 .completion_cb("path", complete_path
),
64 CliCommand
::new(&API_METHOD_SELECT_COMMAND
)
66 .completion_cb("path", complete_path
),
70 CliCommand
::new(&API_METHOD_DESELECT_COMMAND
)
72 .completion_cb("path", complete_path
),
76 CliCommand
::new(&API_METHOD_CLEAR_SELECTED_COMMAND
),
80 CliCommand
::new(&API_METHOD_LIST_SELECTED_COMMAND
),
84 CliCommand
::new(&API_METHOD_RESTORE_SELECTED_COMMAND
)
85 .arg_param(&["target"])
86 .completion_cb("target", crate::tools
::complete_file_name
),
90 CliCommand
::new(&API_METHOD_RESTORE_COMMAND
)
91 .arg_param(&["target"])
92 .completion_cb("target", crate::tools
::complete_file_name
),
96 CliCommand
::new(&API_METHOD_FIND_COMMAND
).arg_param(&["pattern"]),
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
) {
107 eprintln
!("error during completion: {}", err
);
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
125 description
: "target path."
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
142 description
: "target path."
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
158 description
: "target path."
163 /// Read the metadata for a given directory entry.
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
176 description
: "target path."
181 /// Select an entry for restore.
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
194 description
: "path to entry to remove from list."
199 /// Deselect an entry for restore.
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
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
218 description
: "List match patterns instead of the matching files.",
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
235 description
: "Match pattern for matching files in the catalog."
241 description
: "Add matching filenames to list for restore."
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
256 description
: "target path for restore on local filesystem."
261 /// Restore the selected entries to the given target path.
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
273 description
: "target path for restore on local filesystem."
278 description
: "match pattern to limit files for restore."
283 /// Restore the sub-archive given by the current working directory to target.
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
292 /// FIXME: Should we use this to fix `step()`?
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
297 enum PathComponent
<'a
> {
305 struct PathComponentIter
<'a
> {
307 state
: u8, // 0=beginning, 1=ongoing, 2=trailing, 3=finished (fused)
310 impl std
::iter
::FusedIterator
for PathComponentIter
<'_
> {}
312 impl<'a
> Iterator
for PathComponentIter
<'a
> {
313 type Item
= PathComponent
<'a
>;
315 fn next(&mut self) -> Option
<Self::Item
> {
316 if self.path
.is_empty() {
322 if self.path
[0] == b'
/'
{
324 self.path
= &self.path
[1..];
325 return Some(PathComponent
::Root
);
330 let had_slashes
= self.path
[0] == b'
/'
;
331 while self.path
.get(0).copied() == Some(b'
/'
) {
332 self.path
= &self.path
[1..];
335 Some(match self.path
{
336 [] if had_slashes
=> PathComponent
::TrailingSlash
,
338 [b'
.'
] | [b'
.'
, b'
/'
, ..] => {
339 self.path
= &self.path
[1..];
340 PathComponent
::CurDir
342 [b'
.'
, b'
.'
] | [b'
.'
, b'
.'
, b'
/'
, ..] => {
343 self.path
= &self.path
[2..];
344 PathComponent
::ParentDir
350 .position(|&b
| b
== b'
/'
)
351 .unwrap_or(self.path
.len());
352 let (out
, rest
) = self.path
.split_at(end
);
354 PathComponent
::Normal(OsStr
::from_bytes(out
))
361 /// Readline instance handling input and callbacks
362 rl
: rustyline
::Editor
<CliHelper
>,
364 /// Interactive prompt.
367 /// Calalog reader instance to navigate
368 catalog
: CatalogReader
,
370 /// List of selected paths for restore
371 selected
: HashMap
<OsString
, MatchEntry
>,
373 /// pxar accessor instance for the current pxar archive
376 /// The current position in the archive.
377 position
: Vec
<PathStackEntry
>,
381 struct PathStackEntry
{
382 /// This is always available. We mainly navigate through the catalog.
383 catalog
: catalog
::DirEntry
,
385 /// Whenever we need something from the actual archive we fill this out. This is cached along
387 pxar
: Option
<FileEntry
>,
390 impl PathStackEntry
{
391 fn new(dir_entry
: catalog
::DirEntry
) -> Self {
400 /// Create a new shell for the given catalog and pxar archive.
402 mut catalog
: CatalogReader
,
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
));
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
)];
416 let mut this
= Self {
418 prompt
: String
::new(),
420 selected
: HashMap
::new(),
424 this
.update_prompt();
428 async
fn with
<'a
, Fut
, R
, F
>(call
: F
) -> Result
<R
, Error
>
430 F
: FnOnce(&'a
mut Shell
) -> Fut
,
431 Fut
: Future
<Output
= Result
<R
, Error
>>,
436 let shell
: &mut Shell
= unsafe { std::mem::transmute(SHELL.unwrap()) }
;
437 let result
= call(&mut *shell
).await
;
441 pub async
fn shell(mut self) -> Result
<(), Error
> {
442 let this
= &mut self;
444 SHELL
= Some(this
as *mut Shell
as usize);
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
) {
451 println
!("Error: {}", err
);
457 cli
::handle_command_future(helper
.cmd_def(), "", args
, cli
::CliEnvironment
::new())
459 this
.rl
.add_history_entry(line
);
460 this
.update_prompt();
465 fn update_prompt(&mut self) {
466 self.prompt
= "pxar:".to_string();
467 if self.position
.len() <= 1 {
468 self.prompt
.push('
/'
);
470 for p
in self.position
.iter().skip(1) {
471 if !p
.catalog
.name
.starts_with(b
"/") {
472 self.prompt
.push('
/'
);
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>"),
480 self.prompt
.push_str(" > ");
483 async
fn pwd(&mut self) -> Result
<(), Error
> {
484 let stack
= Self::lookup(
492 let path
= Self::format_path_stack(&stack
);
493 println
!("{:?}", path
);
497 fn new_path_stack(&self) -> Vec
<PathStackEntry
> {
498 self.position
[..1].to_vec()
501 async
fn resolve_symlink(
502 stack
: &mut Vec
<PathStackEntry
>,
503 catalog
: &mut CatalogReader
,
505 follow_symlinks
: &mut Option
<usize>,
506 ) -> Result
<(), Error
> {
507 if let Some(ref mut symlink_count
) = follow_symlinks
{
509 if *symlink_count
> MAX_SYMLINK_COUNT
{
510 bail
!("too many levels of symbolic links");
513 let file
= Self::walk_pxar_archive(accessor
, &mut stack
[..]).await?
;
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"),
521 Self::lookup(&stack
, &mut *catalog
, accessor
, Some(path
), follow_symlinks
).await?
;
527 bail
!("target is a symlink");
531 /// Walk a path and add it to the path stack.
533 /// If the symlink count is used, symlinks will be followed, until we hit the cap and error
536 stack
: &mut Vec
<PathStackEntry
>,
537 catalog
: &mut CatalogReader
,
539 component
: std
::path
::Component
<'_
>,
540 follow_symlinks
: &mut Option
<usize>,
541 ) -> Result
<(), Error
> {
542 use std
::path
::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?
;
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?
;
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
),
567 stack
: &mut Vec
<PathStackEntry
>,
568 catalog
: &mut CatalogReader
,
569 component
: std
::path
::Component
<'_
>,
570 ) -> Result
<(), Error
> {
571 use std
::path
::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");
580 Component
::ParentDir
=> drop(stack
.pop()),
581 Component
::Normal(entry
) => {
582 if stack
.last().unwrap().catalog
.is_symlink() {
583 bail
!("target is a symlink");
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
),
595 /// The pxar accessor is required to resolve symbolic links
596 async
fn walk_catalog(
597 stack
: &mut Vec
<PathStackEntry
>,
598 catalog
: &mut CatalogReader
,
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?
;
609 /// Non-async version cannot follow symlinks.
610 fn walk_catalog_nofollow(
611 stack
: &mut Vec
<PathStackEntry
>,
612 catalog
: &mut CatalogReader
,
614 ) -> Result
<(), Error
> {
615 for c
in path
.components() {
616 Self::step_nofollow(stack
, catalog
, c
)?
;
621 /// This assumes that there are no more symlinks in the path stack.
622 async
fn walk_pxar_archive(
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?
);
630 // Now walk the directory stack:
632 while at
< stack
.len() {
633 if stack
[at
].pxar
.is_some() {
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(
644 .ok_or_else(|| format_err
!("no such entry in pxar file: {:?}", name
))?
,
650 Ok(stack
.last().unwrap().pxar
.clone().unwrap())
653 fn complete_path(&mut self, input
: &str) -> Result
<Vec
<String
>, Error
> {
655 let (parent
, base
, part
) = match input
.rfind('
/'
) {
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();
662 tmp_stack
= self.position
.clone();
664 Self::walk_catalog_nofollow(&mut tmp_stack
, &mut self.catalog
, &path
)?
;
665 (&tmp_stack
.last().unwrap().catalog
, base
, part
)
667 None
=> (&self.position
.last().unwrap().catalog
, "", input
),
670 let entries
= self.catalog
.read_dir(parent
)?
;
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() {
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
>>
702 Box
::pin(async
move {
704 None
=> stack
.to_vec(),
706 let mut stack
= if path
.is_absolute() {
711 Self::walk_catalog(&mut stack
, catalog
, accessor
, path
, follow_symlinks
)
719 async
fn ls(&mut self, path
: Option
<&Path
>) -> Result
<(), Error
> {
720 let stack
= Self::lookup(
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();
735 out
.write_all(&item
.name
)?
;
736 out
.write_all(b
"\n")?
;
739 let mut out
= std
::io
::stdout();
740 out
.write_all(&last
.catalog
.name
)?
;
741 out
.write_all(b
"\n")?
;
746 async
fn stat(&mut self, path
: PathBuf
) -> Result
<(), Error
> {
747 let mut stack
= Self::lookup(
756 let file
= Self::walk_pxar_archive(&self.accessor
, &mut stack
).await?
;
758 .write_all(crate::pxar
::format_multi_line_entry(file
.entry()).as_bytes())?
;
762 async
fn cd(&mut self, path
: Option
<&Path
>) -> Result
<(), Error
> {
765 let new_position
= Self::lookup(
773 if !new_position
.last().unwrap().catalog
.is_directory() {
774 bail
!("not a directory");
776 self.position
= new_position
;
778 None
=> self.position
.truncate(1),
780 self.update_prompt();
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("/");
790 let mut out
= OsString
::new();
791 for c
in stack
.iter().skip(1) {
793 out
.push(OsStr
::from_bytes(&c
.catalog
.name
));
799 async
fn select(&mut self, path
: PathBuf
) -> Result
<(), Error
> {
800 let stack
= Self::lookup(
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
);
814 println
!("added path: {:?}", path
);
820 async
fn deselect(&mut self, path
: PathBuf
) -> Result
<(), Error
> {
821 let stack
= Self::lookup(
830 let path
= Self::format_path_stack(&stack
);
832 if self.selected
.remove(&path
).is_some() {
833 println
!("removed path from selection: {:?}", path
);
835 println
!("path not selected: {:?}", path
);
841 async
fn deselect_all(&mut self) -> Result
<(), Error
> {
842 self.selected
.clear();
843 println
!("cleared selection");
847 async
fn list_selected(&mut self, patterns
: bool
) -> Result
<(), Error
> {
849 self.list_selected_patterns().await
851 self.list_matching_files().await
855 async
fn list_selected_patterns(&self) -> Result
<(), Error
> {
856 for entry
in self.selected
.keys() {
857 println
!("{:?}", entry
);
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());
870 async
fn list_matching_files(&mut self) -> Result
<(), Error
> {
871 let matches
= self.build_match_list();
874 &self.position
[0].catalog
,
877 &mut |path
: &[u8]| -> Result
<(), Error
> {
878 let mut out
= std
::io
::stdout();
879 out
.write_all(path
)?
;
880 out
.write_all(b
"\n")?
;
888 async
fn find(&mut self, pattern
: String
, select
: bool
) -> Result
<(), Error
> {
889 let pattern_os
= OsString
::from(pattern
.clone());
891 MatchEntry
::parse_pattern(pattern
, PatternFlag
::PATH_NAME
, MatchType
::Include
)?
;
893 let mut found_some
= false;
895 &self.position
[0].catalog
,
898 &mut |path
: &[u8]| -> Result
<(), Error
> {
900 let mut out
= std
::io
::stdout();
901 out
.write_all(path
)?
;
902 out
.write_all(b
"\n")?
;
907 if found_some
&& select
{
908 self.selected
.insert(pattern_os
, pattern_entry
);
914 async
fn restore_selected(&mut self, destination
: PathBuf
) -> Result
<(), Error
> {
915 if self.selected
.is_empty() {
916 bail
!("no entries selected");
919 let match_list
= self.build_match_list();
921 self.restore_with_match_list(destination
, &match_list
).await
926 destination
: PathBuf
,
927 pattern
: Option
<String
>,
928 ) -> Result
<(), Error
> {
930 let match_list
: &[MatchEntry
] = match pattern
{
933 tmp
= [MatchEntry
::parse_pattern(
935 PatternFlag
::PATH_NAME
,
942 self.restore_with_match_list(destination
, match_list
).await
945 async
fn restore_with_match_list(
947 destination
: PathBuf
,
948 match_list
: &[MatchEntry
],
949 ) -> Result
<(), Error
> {
953 Some(CreateOptions
::new().perm(Mode
::from_bits_truncate(0o700))),
955 .map_err(|err
| format_err
!("error creating directory {:?}: {}", destination
, err
))?
;
957 let rootdir
= Dir
::open(
959 OFlag
::O_DIRECTORY
| OFlag
::O_CLOEXEC
,
963 format_err
!("unable to open target directory {:?}: {}", destination
, err
,)
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
977 let pxar_dir_stack
= PxarDirStack
::new(rootdir
, root_meta
);
979 let mut extractor
= ExtractorState
::new(
988 extractor
.extract().await
997 struct ExtractorState
<'a
> {
1000 path_len_stack
: Vec
<usize>,
1002 dir_stack
: Vec
<PathStackEntry
>,
1005 matches_stack
: Vec
<bool
>,
1007 read_dir
: <Vec
<catalog
::DirEntry
> as IntoIterator
>::IntoIter
,
1008 read_dir_stack
: Vec
<<Vec
<catalog
::DirEntry
> as IntoIterator
>::IntoIter
>,
1010 pxar_dir_stack
: PxarDirStack
,
1012 catalog
: &'a
mut CatalogReader
,
1014 match_list
: &'a
[MatchEntry
],
1015 accessor
: &'a Accessor
,
1018 impl<'a
> ExtractorState
<'a
> {
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
)?
1033 path_len_stack
: Vec
::new(),
1037 matches
: match_list
.is_empty(),
1038 matches_stack
: Vec
::new(),
1041 read_dir_stack
: Vec
::new(),
1052 pub async
fn extract(&mut self) -> Result
<(), Error
> {
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,
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'
/'
);
1067 self.path
.extend(&entry
.name
);
1069 self.handle_entry(entry
).await?
;
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() {
1079 None
=> return Ok(LoopState
::Break
), // out of root directory
1085 .ok_or_else(|| format_err
!("internal iterator error (matches_stack)"))?
;
1089 .ok_or_else(|| format_err
!("internal iterator error (dir_stack)"))?
;
1094 .ok_or_else(|| format_err
!("internal iterator error (pxar_dir_stack)"))?
;
1096 self.path_len
= self
1099 .ok_or_else(|| format_err
!("internal iterator error (path_len_stack)"))?
;
1102 let dirname
= CStr
::from_bytes_with_nul(&self.path
[(self.path_len
+ 1)..])?
;
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
)?
;
1109 Ok(LoopState
::Continue
)
1112 async
fn handle_new_directory(
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(
1120 self.catalog
.read_dir(&entry
)?
.into_iter(),
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();
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();
1131 .push(dir_pxar
.file_name().to_os_string(), dir_meta
)?
;
1133 if self.matches
&& match_result
!= Some(MatchType
::Exclude
) {
1134 todo
!("create this directory");
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
,
1148 match (did_match
, &entry
.attr
) {
1149 (_
, DirEntryAttribute
::Directory { .. }
) => {
1150 self.handle_new_directory(entry
, match_result
).await?
;
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();
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();
1170 (false, _
) => (), // skip
1176 fn path(&self) -> &OsStr
{
1177 OsStr
::from_bytes(&self.path
)
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?
;
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(
1190 OFlag
::O_CREAT
| OFlag
::O_WRONLY
| OFlag
::O_CLOEXEC
,
1191 Mode
::from_bits(0o600).unwrap(),
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
);
1200 metadata
::apply_with_path(
1211 "catalog file {:?} not a regular file in the archive",
1218 async
fn extract_special(
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())
1227 (DirEntryAttribute
::Symlink
, _
) => {
1229 "catalog symlink {:?} not a symlink in the archive",
1234 (DirEntryAttribute
::Hardlink
, pxar
::EntryKind
::Hardlink(hardlink
)) => {
1235 self.extract_hardlink(entry
.file_name(), hardlink
.as_os_str(), entry
.metadata())
1237 (DirEntryAttribute
::Hardlink
, _
) => {
1239 "catalog hardlink {:?} not a hardlink in the archive",
1244 (ref attr
, pxar
::EntryKind
::Device(device
)) => {
1245 self.extract_device(attr
.clone(), entry
.file_name(), device
, entry
.metadata())
1248 (DirEntryAttribute
::Fifo
, pxar
::EntryKind
::Fifo
) => {
1249 self.extract_node(entry
.file_name(), 0, entry
.metadata())
1251 (DirEntryAttribute
::Fifo
, _
) => {
1252 bail
!("catalog fifo {:?} not a fifo in the archive", self.path());
1255 (DirEntryAttribute
::Socket
, pxar
::EntryKind
::Socket
) => {
1256 self.extract_node(entry
.file_name(), 0, entry
.metadata())
1258 (DirEntryAttribute
::Socket
, _
) => {
1260 "catalog socket {:?} not a socket in the archive",
1265 attr
=> bail
!("unhandled file type {:?} for {:?}", attr
, self.path()),
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
)?
;
1282 &CString
::new(file_name
.as_bytes())?
,
1288 fn extract_hardlink(
1292 _metadata
: &Metadata
,
1293 ) -> Result
<(), Error
> {
1294 crate::pxar
::tools
::assert_relative_path(target
)?
;
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(
1303 nix
::unistd
::LinkatFlags
::NoSymlinkFollow
,
1311 attr
: DirEntryAttribute
,
1313 device
: &pxar
::format
::Device
,
1314 metadata
: &Metadata
,
1315 ) -> Result
<(), Error
> {
1317 DirEntryAttribute
::BlockDevice
=> {
1318 if !metadata
.stat
.is_blockdev() {
1320 "catalog block device {:?} is not a block device in the archive",
1325 DirEntryAttribute
::CharDevice
=> {
1326 if !metadata
.stat
.is_chardev() {
1328 "catalog character device {:?} is not a character device in the archive",
1335 "unexpected file type for {:?} in the catalog, \
1336 which is a device special file in the archive",
1341 self.extract_node(file_name
, device
.to_dev_t(), metadata
)
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(|_
| {
1353 "device node's mode contains illegal bits: 0x{:x} (0o{:o})",
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
))?
;
1364 metadata
::apply_at(self.feature_flags
, metadata
, parent
, &file_name
)