]> git.proxmox.com Git - proxmox-backup.git/blame - src/backup/catalog_shell.rs
use proxmox 0.1.9 with new cli command helpers
[proxmox-backup.git] / src / backup / catalog_shell.rs
CommitLineData
6934c6fe 1use std::cell::RefCell;
25cdd0e0 2use std::collections::HashMap;
6934c6fe 3use std::ffi::{CString, OsStr};
f14c96ea
CE
4use std::io::Write;
5use std::os::unix::ffi::OsStrExt;
6use std::path::Path;
7
8use failure::*;
f14c96ea 9
6934c6fe 10use super::catalog::{CatalogReader, DirEntry};
f14c96ea 11use crate::pxar::*;
6934c6fe 12use crate::tools;
f14c96ea 13
6934c6fe 14use proxmox::api::{cli::*, *};
f14c96ea 15
951cf17e 16const PROMPT_PREFIX: &str = "pxar:";
6934c6fe 17const PROMPT: &str = ">";
951cf17e
CE
18
19/// Interactive shell for interacton with the catalog.
f14c96ea 20pub struct Shell {
6934c6fe
CE
21 /// Readline instance handling input and callbacks
22 rl: rustyline::Editor<CliHelper>,
23 prompt: String,
951cf17e
CE
24}
25
ecbaa38f
DM
26/// This list defines all the shell commands and their properties
27/// using the api schema
55c3cb69 28pub fn catalog_shell_cli() -> CommandLineInterface {
ecbaa38f
DM
29
30 let map = CliCommandMap::new()
48ef3c33 31 .insert("pwd", CliCommand::new(&API_METHOD_PWD_COMMAND))
ecbaa38f
DM
32 .insert(
33 "cd",
34 CliCommand::new(&API_METHOD_CD_COMMAND)
35 .arg_param(&["path"])
36 .completion_cb("path", Shell::complete_path)
ecbaa38f
DM
37 )
38 .insert(
39 "ls",
40 CliCommand::new(&API_METHOD_LS_COMMAND)
41 .arg_param(&["path"])
42 .completion_cb("path", Shell::complete_path)
48ef3c33 43 )
ecbaa38f
DM
44 .insert(
45 "stat",
46 CliCommand::new(&API_METHOD_STAT_COMMAND)
47 .arg_param(&["path"])
48 .completion_cb("path", Shell::complete_path)
48ef3c33 49 )
ecbaa38f
DM
50 .insert(
51 "select",
52 CliCommand::new(&API_METHOD_SELECT_COMMAND)
53 .arg_param(&["path"])
54 .completion_cb("path", Shell::complete_path)
ecbaa38f
DM
55 )
56 .insert(
57 "deselect",
58 CliCommand::new(&API_METHOD_DESELECT_COMMAND)
59 .arg_param(&["path"])
60 .completion_cb("path", Shell::complete_path)
ecbaa38f
DM
61 )
62 .insert(
63 "restore-selected",
64 CliCommand::new(&API_METHOD_RESTORE_SELECTED_COMMAND)
65 .arg_param(&["target"])
66 .completion_cb("target", tools::complete_file_name)
ecbaa38f
DM
67 )
68 .insert(
69 "list-selected",
48ef3c33 70 CliCommand::new(&API_METHOD_LIST_SELECTED_COMMAND),
ecbaa38f
DM
71 )
72 .insert(
73 "restore",
74 CliCommand::new(&API_METHOD_RESTORE_COMMAND)
75 .arg_param(&["target"])
76 .completion_cb("target", tools::complete_file_name)
ecbaa38f 77 )
25cdd0e0
CE
78 .insert(
79 "find",
80 CliCommand::new(&API_METHOD_FIND_COMMAND)
81 .arg_param(&["path", "pattern"])
82 .completion_cb("path", Shell::complete_path)
83 )
ecbaa38f
DM
84 .insert_help();
85
86 CommandLineInterface::Nested(map)
87}
88
951cf17e
CE
89impl Shell {
90 /// Create a new shell for the given catalog and pxar archive.
91 pub fn new(
6934c6fe 92 mut catalog: CatalogReader<std::fs::File>,
951cf17e
CE
93 archive_name: &str,
94 decoder: Decoder,
95 ) -> Result<Self, Error> {
6934c6fe
CE
96 let catalog_root = catalog.root()?;
97 // The root for the given archive as stored in the catalog
98 let archive_root = catalog.lookup(&catalog_root, archive_name.as_bytes())?;
99 let root = vec![archive_root];
100
101 CONTEXT.with(|handle| {
102 let mut ctx = handle.borrow_mut();
103 *ctx = Some(Context {
104 catalog,
25cdd0e0 105 selected: Vec::new(),
6934c6fe
CE
106 decoder,
107 root: root.clone(),
108 current: root,
109 });
110 });
111
55c3cb69 112 let cli_helper = CliHelper::new(catalog_shell_cli());
6934c6fe
CE
113 let mut rl = rustyline::Editor::<CliHelper>::new();
114 rl.set_helper(Some(cli_helper));
115
116 Context::with(|ctx| {
117 Ok(Self {
118 rl,
119 prompt: ctx.generate_prompt()?,
120 })
951cf17e
CE
121 })
122 }
123
124 /// Start the interactive shell loop
125 pub fn shell(mut self) -> Result<(), Error> {
6934c6fe
CE
126 while let Ok(line) = self.rl.readline(&self.prompt) {
127 let helper = self.rl.helper().unwrap();
128 let args = match shellword_split(&line) {
129 Ok(args) => args,
951cf17e 130 Err(err) => {
6934c6fe 131 println!("Error: {}", err);
951cf17e
CE
132 continue;
133 }
134 };
d08bc483 135 let _ = handle_command(helper.cmd_def(), "", args, None);
6934c6fe
CE
136 self.rl.add_history_entry(line);
137 self.update_prompt()?;
951cf17e
CE
138 }
139 Ok(())
140 }
951cf17e 141
6934c6fe
CE
142 /// Update the prompt to the new working directory
143 fn update_prompt(&mut self) -> Result<(), Error> {
144 Context::with(|ctx| {
145 self.prompt = ctx.generate_prompt()?;
146 Ok(())
147 })
951cf17e
CE
148 }
149
6934c6fe
CE
150 /// Completions for paths by lookup in the catalog
151 fn complete_path(complete_me: &str, _map: &HashMap<String, String>) -> Vec<String> {
152 Context::with(|ctx| {
153 let (base, to_complete) = match complete_me.rfind('/') {
154 // Split at ind + 1 so the slash remains on base, ok also if
155 // ends in slash as split_at accepts up to length as index.
156 Some(ind) => complete_me.split_at(ind + 1),
157 None => ("", complete_me),
158 };
951cf17e 159
6934c6fe
CE
160 let current = if base.is_empty() {
161 ctx.current.clone()
162 } else {
163 ctx.canonical_path(base)?
164 };
951cf17e 165
6934c6fe
CE
166 let entries = match ctx.catalog.read_dir(&current.last().unwrap()) {
167 Ok(entries) => entries,
168 Err(_) => return Ok(Vec::new()),
169 };
170
171 let mut list = Vec::new();
172 for entry in &entries {
173 let mut name = String::from(base);
174 if entry.name.starts_with(to_complete.as_bytes()) {
175 name.push_str(std::str::from_utf8(&entry.name)?);
176 if entry.is_directory() {
177 name.push('/');
951cf17e 178 }
6934c6fe 179 list.push(name);
951cf17e 180 }
951cf17e 181 }
6934c6fe
CE
182 Ok(list)
183 })
184 .unwrap_or_default()
951cf17e
CE
185 }
186}
187
6934c6fe
CE
188#[api(input: { properties: {} })]
189/// List the current working directory.
190fn pwd_command() -> Result<(), Error> {
191 Context::with(|ctx| {
192 let path = Context::generate_cstring(&ctx.current)?;
193 let mut out = std::io::stdout();
194 out.write_all(&path.as_bytes())?;
195 out.write_all(&[b'\n'])?;
196 out.flush()?;
197 Ok(())
198 })
951cf17e
CE
199}
200
6934c6fe
CE
201#[api(
202 input: {
203 properties: {
204 path: {
205 type: String,
206 optional: true,
207 description: "target path."
208 }
209 }
951cf17e 210 }
6934c6fe
CE
211)]
212/// Change the current working directory to the new directory
213fn cd_command(path: Option<String>) -> Result<(), Error> {
214 Context::with(|ctx| {
215 let path = path.unwrap_or_default();
216 let mut path = ctx.canonical_path(&path)?;
217 if !path
218 .last()
219 .ok_or_else(|| format_err!("invalid path component"))?
220 .is_directory()
221 {
222 // Change to the parent dir of the file instead
223 path.pop();
224 eprintln!("not a directory, fallback to parent directory");
225 }
226 ctx.current = path;
227 Ok(())
228 })
951cf17e
CE
229}
230
6934c6fe
CE
231#[api(
232 input: {
233 properties: {
234 path: {
235 type: String,
236 optional: true,
237 description: "target path."
238 }
951cf17e
CE
239 }
240 }
6934c6fe
CE
241)]
242/// List the content of working directory or given path.
243fn ls_command(path: Option<String>) -> Result<(), Error> {
244 Context::with(|ctx| {
245 let parent = if let Some(path) = path {
246 ctx.canonical_path(&path)?
247 .last()
248 .ok_or_else(|| format_err!("invalid path component"))?
249 .clone()
250 } else {
251 ctx.current.last().unwrap().clone()
252 };
951cf17e 253
6934c6fe
CE
254 let list = if parent.is_directory() {
255 ctx.catalog.read_dir(&parent)?
951cf17e 256 } else {
6934c6fe
CE
257 vec![parent]
258 };
259
260 if list.is_empty() {
261 return Ok(());
951cf17e 262 }
6934c6fe
CE
263 let max = list.iter().max_by(|x, y| x.name.len().cmp(&y.name.len()));
264 let max = match max {
265 Some(dir_entry) => dir_entry.name.len() + 1,
266 None => 0,
267 };
951cf17e 268
6934c6fe
CE
269 let (_rows, mut cols) = Context::get_terminal_size();
270 cols /= max;
951cf17e 271
6934c6fe
CE
272 let mut out = std::io::stdout();
273 for (index, item) in list.iter().enumerate() {
274 out.write_all(&item.name)?;
275 // Fill with whitespaces
276 out.write_all(&vec![b' '; max - item.name.len()])?;
277 if index % cols == (cols - 1) {
278 out.write_all(&[b'\n'])?;
951cf17e 279 }
951cf17e 280 }
6934c6fe
CE
281 // If the last line is not complete, add the newline
282 if list.len() % cols != cols - 1 {
283 out.write_all(&[b'\n'])?;
951cf17e 284 }
6934c6fe
CE
285 out.flush()?;
286 Ok(())
287 })
288}
289
290#[api(
291 input: {
292 properties: {
293 path: {
294 type: String,
295 description: "target path."
296 }
951cf17e 297 }
951cf17e 298 }
6934c6fe
CE
299)]
300/// Read the metadata for a given directory entry.
301///
302/// This is expensive because the data has to be read from the pxar `Decoder`,
303/// which means reading over the network.
304fn stat_command(path: String) -> Result<(), Error> {
305 Context::with(|ctx| {
306 // First check if the file exists in the catalog, therefore avoiding
307 // expensive calls to the decoder just to find out that there maybe is
308 // no such entry.
309 // This is done by calling canonical_path(), which returns the full path
310 // if it exists, error otherwise.
311 let path = ctx.canonical_path(&path)?;
90fc97af 312 let item = ctx.lookup(&path)?;
6934c6fe
CE
313 let mut out = std::io::stdout();
314 out.write_all(b"File: ")?;
315 out.write_all(item.filename.as_bytes())?;
316 out.write_all(&[b'\n'])?;
90fc97af 317 out.write_all(format!("Size: {}\n", item.size).as_bytes())?;
6934c6fe
CE
318 out.write_all(b"Type: ")?;
319 match item.entry.mode as u32 & libc::S_IFMT {
320 libc::S_IFDIR => out.write_all(b"directory\n")?,
321 libc::S_IFREG => out.write_all(b"regular file\n")?,
322 libc::S_IFLNK => out.write_all(b"symbolic link\n")?,
323 libc::S_IFBLK => out.write_all(b"block special file\n")?,
324 libc::S_IFCHR => out.write_all(b"character special file\n")?,
325 _ => out.write_all(b"unknown\n")?,
326 };
327 out.write_all(format!("Uid: {}\n", item.entry.uid).as_bytes())?;
328 out.write_all(format!("Gid: {}\n", item.entry.gid).as_bytes())?;
329 out.flush()?;
330 Ok(())
331 })
951cf17e
CE
332}
333
6934c6fe
CE
334#[api(
335 input: {
336 properties: {
337 path: {
338 type: String,
339 description: "target path."
340 }
341 }
342 }
343)]
344/// Select an entry for restore.
345///
346/// This will return an error if the entry is already present in the list or
347/// if an invalid path was provided.
348fn select_command(path: String) -> Result<(), Error> {
349 Context::with(|ctx| {
350 // Calling canonical_path() makes sure the provided path is valid and
351 // actually contained within the catalog and therefore also the archive.
352 let path = ctx.canonical_path(&path)?;
25cdd0e0
CE
353 let pattern = MatchPattern::from_line(Context::generate_cstring(&path)?.as_bytes())?
354 .ok_or_else(|| format_err!("encountered invalid match pattern"))?;
355 if ctx.selected.iter().find(|p| **p == pattern).is_none() {
356 ctx.selected.push(pattern);
6934c6fe 357 }
25cdd0e0 358 Ok(())
6934c6fe 359 })
f14c96ea
CE
360}
361
6934c6fe
CE
362#[api(
363 input: {
364 properties: {
365 path: {
366 type: String,
367 description: "path to entry to remove from list."
368 }
369 }
f14c96ea 370 }
6934c6fe
CE
371)]
372/// Deselect an entry for restore.
373///
374/// This will return an error if the entry was not found in the list of entries
375/// selected for restore.
376fn deselect_command(path: String) -> Result<(), Error> {
377 Context::with(|ctx| {
378 let path = ctx.canonical_path(&path)?;
25cdd0e0
CE
379 let mut pattern = MatchPattern::from_line(Context::generate_cstring(&path)?.as_bytes())?
380 .ok_or_else(|| format_err!("encountered invalid match pattern"))?;
381 if let Some(last) = ctx.selected.last() {
382 if last == &pattern {
383 ctx.selected.pop();
384 return Ok(());
385 }
6934c6fe 386 }
25cdd0e0
CE
387 pattern.invert();
388 ctx.selected.push(pattern);
389 Ok(())
6934c6fe
CE
390 })
391}
f14c96ea 392
6934c6fe
CE
393#[api(
394 input: {
395 properties: {
396 target: {
397 type: String,
398 description: "target path for restore on local filesystem."
399 }
400 }
f14c96ea 401 }
6934c6fe
CE
402)]
403/// Restore the selected entries to the given target path.
404///
405/// Target must not exist on the clients filesystem.
406fn restore_selected_command(target: String) -> Result<(), Error> {
407 Context::with(|ctx| {
25cdd0e0 408 if ctx.selected.is_empty() {
6934c6fe
CE
409 bail!("no entries selected for restore");
410 }
f14c96ea 411
6934c6fe
CE
412 // Entry point for the restore is always root here as the provided match
413 // patterns are relative to root as well.
414 let start_dir = ctx.decoder.root()?;
415 ctx.decoder
25cdd0e0 416 .restore(&start_dir, &Path::new(&target), &ctx.selected)?;
6934c6fe
CE
417 Ok(())
418 })
419}
f14c96ea 420
6934c6fe
CE
421#[api( input: { properties: {} })]
422/// List entries currently selected for restore.
423fn list_selected_command() -> Result<(), Error> {
424 Context::with(|ctx| {
425 let mut out = std::io::stdout();
25cdd0e0 426 out.write_all(&MatchPattern::to_bytes(ctx.selected.as_slice()))?;
6934c6fe 427 out.flush()?;
f14c96ea 428 Ok(())
6934c6fe
CE
429 })
430}
f14c96ea 431
6934c6fe
CE
432#[api(
433 input: {
434 properties: {
435 target: {
436 type: String,
437 description: "target path for restore on local filesystem."
438 },
439 pattern: {
440 type: String,
441 optional: true,
442 description: "match pattern to limit files for restore."
443 }
444 }
445 }
446)]
447/// Restore the sub-archive given by the current working directory to target.
448///
449/// By further providing a pattern, the restore can be limited to a narrower
450/// subset of this sub-archive.
451/// If pattern is not present or empty, the full archive is restored to target.
452fn restore_command(target: String, pattern: Option<String>) -> Result<(), Error> {
453 Context::with(|ctx| {
454 let pattern = pattern.unwrap_or_default();
455 let match_pattern = match pattern.as_str() {
456 "" | "/" | "." => Vec::new(),
457 _ => vec![MatchPattern::from_line(pattern.as_bytes())?.unwrap()],
f14c96ea 458 };
6934c6fe
CE
459 // Decoder entry point for the restore.
460 let start_dir = if pattern.starts_with("/") {
461 ctx.decoder.root()?
f14c96ea 462 } else {
6934c6fe
CE
463 // Get the directory corresponding to the working directory from the
464 // archive.
465 let cwd = ctx.current.clone();
90fc97af 466 ctx.lookup(&cwd)?
f14c96ea 467 };
f14c96ea 468
6934c6fe
CE
469 ctx.decoder
470 .restore(&start_dir, &Path::new(&target), &match_pattern)?;
471 Ok(())
472 })
473}
474
25cdd0e0
CE
475#[api(
476 input: {
477 properties: {
478 path: {
479 type: String,
480 description: "Path to node from where to start the search."
481 },
482 pattern: {
483 type: String,
484 description: "Match pattern for matching files in the catalog."
485 },
486 select: {
487 type: bool,
488 optional: true,
489 description: "Add matching filenames to list for restore."
490 }
491 }
492 }
493)]
494/// Find entries in the catalog matching the given match pattern.
495fn find_command(path: String, pattern: String, select: Option<bool>) -> Result<(), Error> {
496 Context::with(|ctx| {
38d9a698 497 let path = ctx.canonical_path(&path)?;
25cdd0e0
CE
498 if !path.last().unwrap().is_directory() {
499 bail!("path should be a directory, not a file!");
500 }
501 let select = select.unwrap_or(false);
502
503 let cpath = Context::generate_cstring(&path).unwrap();
504 let pattern = if pattern.starts_with("!") {
505 let mut buffer = vec![b'!'];
506 buffer.extend_from_slice(cpath.as_bytes());
507 buffer.extend_from_slice(pattern[1..pattern.len()].as_bytes());
508 buffer
509 } else {
510 let mut buffer = cpath.as_bytes().to_vec();
511 buffer.extend_from_slice(pattern.as_bytes());
512 buffer
513 };
514
515 let pattern = MatchPattern::from_line(&pattern)?
516 .ok_or_else(|| format_err!("invalid match pattern"))?;
517 let slice = vec![pattern.as_slice()];
518
38d9a698
CE
519 // The match pattern all contain the prefix of the entry path in order to
520 // store them if selected, so the entry point for find is always the root
521 // directory.
522 let mut dir_stack = ctx.root.clone();
25cdd0e0 523 ctx.catalog.find(
38d9a698 524 &mut dir_stack,
25cdd0e0
CE
525 &slice,
526 &Box::new(|path: &[DirEntry]| println!("{:?}", Context::generate_cstring(path).unwrap()))
527 )?;
528
529 // Insert if matches should be selected.
530 // Avoid duplicate entries of the same match pattern.
531 if select && ctx.selected.iter().find(|p| **p == pattern).is_none() {
532 ctx.selected.push(pattern);
533 }
534
535 Ok(())
536 })
537}
538
6934c6fe
CE
539std::thread_local! {
540 static CONTEXT: RefCell<Option<Context>> = RefCell::new(None);
541}
542
543/// Holds the context needed for access to catalog and decoder
544struct Context {
545 /// Calalog reader instance to navigate
546 catalog: CatalogReader<std::fs::File>,
547 /// List of selected paths for restore
25cdd0e0 548 selected: Vec<MatchPattern>,
6934c6fe
CE
549 /// Decoder instance for the current pxar archive
550 decoder: Decoder,
551 /// Root directory for the give archive as stored in the catalog
552 root: Vec<DirEntry>,
553 /// Stack of directories up to the current working directory
554 /// used for navigation and path completion.
555 current: Vec<DirEntry>,
556}
557
558impl Context {
559 /// Execute `call` within a context providing a mut ref to `Context` instance.
560 fn with<T, F>(call: F) -> Result<T, Error>
561 where
562 F: FnOnce(&mut Context) -> Result<T, Error>,
563 {
564 CONTEXT.with(|cell| {
565 let mut ctx = cell.borrow_mut();
566 call(&mut ctx.as_mut().unwrap())
567 })
f14c96ea
CE
568 }
569
6934c6fe
CE
570 /// Generate CString from provided stack of `DirEntry`s.
571 fn generate_cstring(dir_stack: &[DirEntry]) -> Result<CString, Error> {
f14c96ea 572 let mut path = vec![b'/'];
6934c6fe
CE
573 // Skip the archive root, the '/' is displayed for it instead
574 for component in dir_stack.iter().skip(1) {
575 path.extend_from_slice(&component.name);
576 if component.is_directory() {
f14c96ea
CE
577 path.push(b'/');
578 }
579 }
6934c6fe 580 Ok(unsafe { CString::from_vec_unchecked(path) })
f14c96ea
CE
581 }
582
583 /// Resolve the indirect path components and return an absolute path.
584 ///
585 /// This will actually navigate the filesystem tree to check that the
586 /// path is vaild and exists.
587 /// This does not include following symbolic links.
588 /// If None is given as path, only the root directory is returned.
6934c6fe
CE
589 fn canonical_path(&mut self, path: &str) -> Result<Vec<DirEntry>, Error> {
590 if path == "/" {
f14c96ea
CE
591 return Ok(self.root.clone());
592 }
593
594 let mut path_slice = if path.is_empty() {
595 // Fallback to root if no path was provided
596 return Ok(self.root.clone());
597 } else {
598 path
599 };
600
6934c6fe 601 let mut dir_stack = if path_slice.starts_with("/") {
f14c96ea
CE
602 // Absolute path, reduce view of slice and start from root
603 path_slice = &path_slice[1..];
604 self.root.clone()
605 } else {
606 // Relative path, start from current working directory
6934c6fe 607 self.current.clone()
f14c96ea 608 };
6934c6fe 609 let should_end_dir = if path_slice.ends_with("/") {
f14c96ea
CE
610 path_slice = &path_slice[0..path_slice.len() - 1];
611 true
612 } else {
613 false
614 };
6934c6fe 615 for name in path_slice.split('/') {
f14c96ea 616 match name {
6934c6fe
CE
617 "." => continue,
618 ".." => {
f14c96ea
CE
619 // Never pop archive root from stack
620 if dir_stack.len() > 1 {
621 dir_stack.pop();
622 }
623 }
624 _ => {
6934c6fe 625 let entry = self.catalog.lookup(dir_stack.last().unwrap(), name.as_bytes())?;
f14c96ea
CE
626 dir_stack.push(entry);
627 }
628 }
629 }
951cf17e
CE
630 if should_end_dir
631 && !dir_stack
632 .last()
633 .ok_or_else(|| format_err!("invalid path component"))?
634 .is_directory()
f14c96ea
CE
635 {
636 bail!("entry is not a directory");
637 }
638
639 Ok(dir_stack)
640 }
641
6934c6fe
CE
642 /// Generate the CString to display by readline based on
643 /// PROMPT_PREFIX, PROMPT and the current working directory.
644 fn generate_prompt(&self) -> Result<String, Error> {
645 let prompt = format!(
646 "{}{} {} ",
647 PROMPT_PREFIX,
648 Self::generate_cstring(&self.current)?.to_string_lossy(),
649 PROMPT,
650 );
651 Ok(prompt)
f14c96ea
CE
652 }
653
654 /// Get the current size of the terminal
314bb358 655 /// # Safety
f14c96ea 656 ///
314bb358 657 /// uses unsafe call to tty_ioctl, see man tty_ioctl(2)
f14c96ea 658 fn get_terminal_size() -> (usize, usize) {
314bb358 659 const TIOCGWINSZ: libc::c_ulong = 0x5413;
f14c96ea
CE
660
661 #[repr(C)]
662 struct WinSize {
663 ws_row: libc::c_ushort,
664 ws_col: libc::c_ushort,
665 _ws_xpixel: libc::c_ushort, // unused
666 _ws_ypixel: libc::c_ushort, // unused
667 }
668
669 let mut winsize = WinSize {
670 ws_row: 0,
671 ws_col: 0,
672 _ws_xpixel: 0,
673 _ws_ypixel: 0,
674 };
675 unsafe { libc::ioctl(libc::STDOUT_FILENO, TIOCGWINSZ, &mut winsize) };
676 (winsize.ws_row as usize, winsize.ws_col as usize)
677 }
f14c96ea 678
6934c6fe
CE
679 /// Look up the entry given by a canonical absolute `path` in the archive.
680 ///
681 /// This will actively navigate the archive by calling the corresponding
682 /// decoder functionalities and is therefore very expensive.
90fc97af 683 fn lookup(&mut self, absolute_path: &[DirEntry]) -> Result<DirectoryEntry, Error> {
6934c6fe 684 let mut current = self.decoder.root()?;
6934c6fe
CE
685 // Ignore the archive root, don't need it.
686 for item in absolute_path.iter().skip(1) {
687 match self
688 .decoder
689 .lookup(&current, &OsStr::from_bytes(&item.name))?
690 {
90fc97af 691 Some(item) => current = item,
6934c6fe
CE
692 // This should not happen if catalog an archive are consistent.
693 None => bail!("no such file or directory in archive - inconsistent catalog"),
f14c96ea 694 }
f14c96ea 695 }
90fc97af 696 Ok(current)
f14c96ea 697 }
f14c96ea 698}