From 6934c6fe7757142a336e531a113fd837a1031fda Mon Sep 17 00:00:00 2001 From: Christian Ebner Date: Thu, 5 Dec 2019 17:23:21 +0100 Subject: [PATCH] src/backup/catalog_shell.rs: adapt to use API Schema definition and rustyline This major refactoring of the catalog based shell utilizes the new API macro and the API Schema as well as rustyline instead of the old GNU readline C API. The code now has these 3 main components: * The `Shell` which handles the readline loop via rustyline. * The shell functions defined via the API macro. * The `Context` which holds catalog and decoder instances. Signed-off-by: Christian Ebner --- src/backup/catalog_shell.rs | 1134 +++++++++++++++-------------------- 1 file changed, 498 insertions(+), 636 deletions(-) diff --git a/src/backup/catalog_shell.rs b/src/backup/catalog_shell.rs index 7e170f7a..5ee11cba 100644 --- a/src/backup/catalog_shell.rs +++ b/src/backup/catalog_shell.rs @@ -1,324 +1,30 @@ +use std::cell::RefCell; use std::collections::{HashMap, HashSet}; -use std::ffi::{CStr, CString, OsStr}; +use std::ffi::{CString, OsStr}; use std::io::Write; use std::os::unix::ffi::OsStrExt; use std::path::Path; use failure::*; -use libc; - -use crate::pxar::*; use super::catalog::{CatalogReader, DirEntry}; -use super::readline::{Context, Readline}; +use crate::pxar::*; +use crate::tools; + +use proxmox::api::{cli::*, *}; const PROMPT_PREFIX: &str = "pxar:"; -const PROMPT_POST: &str = " > "; +const PROMPT: &str = ">"; /// Interactive shell for interacton with the catalog. pub struct Shell { - /// Actual shell instance with context. - sh: ShellInstance, - /// Map containing all the defined commands. - cmds: ShellCmdMap, + /// Readline instance handling input and callbacks + rl: rustyline::Editor, + prompt: String, } impl Shell { /// Create a new shell for the given catalog and pxar archive. - pub fn new( - catalog: CatalogReader, - archive_name: &str, - decoder: Decoder, - ) -> Result { - const OPTIONAL: bool = true; - const REQUIRED: bool = false; - - Ok(Self { - sh: ShellInstance::new(catalog, archive_name, decoder)?, - // This list defines all the commands for the shell including their - // parameters, options and help description. - cmds: ShellCmdMap::new() - .insert(ShellCmd::new( - "pwd", - "List the current working directory.", - ShellInstance::pwd, - )) - .insert(ShellCmd::new( - "ls", - "List contents of directory.", - ShellInstance::list, - ).parameter("path", OPTIONAL)) - .insert(ShellCmd::new( - "cd", - "Change current working directory.", - ShellInstance::change_dir, - ).parameter("path", OPTIONAL)) - .insert(ShellCmd::new( - "stat", - "Show the status of a file or directory.", - ShellInstance::stat, - ).parameter("path", REQUIRED)) - .insert(ShellCmd::new( - "restore", - "Restore archive to target (restores only matching entries if match-pattern is provided)", - ShellInstance::restore, - ).option("match", Some("match-pattern")).parameter("target", REQUIRED)) - .insert(ShellCmd::new( - "select", - "Add a file/directory to the list of entries selected for restore.", - ShellInstance::select, - ).parameter("path", REQUIRED)) - .insert(ShellCmd::new( - "selected", - "Show the list of entries currently selected for restore.", - ShellInstance::list_selected, - )) - .insert(ShellCmd::new( - "deselect", - "Remove a file/directory from the list of entries selected for restore.", - ShellInstance::deselect, - ).parameter("path", REQUIRED)) - .insert(ShellCmd::new( - "restore-selected", - "Restore the file/directory on the list of entries selected for restore.", - ShellInstance::restore_selected, - ).parameter("target", REQUIRED)) - .insert(ShellCmd::new( - "help", - "Show all commands or the help for the provided command", - ShellInstance::help, - ).parameter("command", OPTIONAL)) - }) - } - - /// Start the interactive shell loop - pub fn shell(mut self) -> Result<(), Error> { - while let Some(line) = self.sh.rl.readline() { - let (cmd, args) = match self.cmds.parse(&line) { - Ok(res) => res, - Err(err) => { - println!("error: {}", err); - continue; - } - }; - // Help is treated a bit separate as we need the full command list, - // which would not be accessible in the callback. - if cmd.command == "help" { - match args.get_param("command") { - Some(name) => match self.cmds.cmds.get(name) { - Some(cmd) => println!("{}", cmd.help()), - None => println!("no help for command"), - }, - None => self.cmds.list_commands(), - } - } - match (cmd.callback)(&mut self.sh, args) { - Ok(_) => (), - Err(err) => { - println!("error: {}", err); - continue; - } - }; - } - Ok(()) - } -} - -/// Stores the command definitions for the known commands. -struct ShellCmdMap { - cmds: HashMap<&'static [u8], ShellCmd>, -} - -impl ShellCmdMap { - fn new() -> Self { - Self { - cmds: HashMap::new(), - } - } - - /// Insert a new `ShellCmd` into the `ShellCmdMap` - fn insert(mut self, cmd: ShellCmd) -> Self { - self.cmds.insert(cmd.command.as_bytes(), cmd); - self - } - - /// List all known commands with their help text. - fn list_commands(&self) { - println!(); - for cmd in &self.cmds { - println!("{}\n", cmd.1.help()); - } - } - - /// Parse the given line and interprete it based on the known commands in - /// this `ShellCmdMap` instance. - fn parse<'a>(&'a self, line: &'a [u8]) -> Result<(&'a ShellCmd, Args), Error> { - // readline already handles tabs, so here we only split on spaces - let args: Vec<&[u8]> = line - .split(|b| *b == b' ') - .filter(|word| !word.is_empty()) - .collect(); - let mut args = args.iter(); - let arg0 = args - .next() - .ok_or_else(|| format_err!("no command provided"))?; - let cmd = self - .cmds - .get(arg0) - .ok_or_else(|| format_err!("invalid command"))?; - let mut given = Args { - options: HashMap::new(), - parameters: HashMap::new(), - }; - let mut required = cmd.required_parameters.iter(); - let mut optional = cmd.optional_parameters.iter(); - while let Some(arg) = args.next() { - if arg.starts_with(b"--") { - let opt = cmd - .options - .iter() - .find(|opt| opt.0.as_bytes() == &arg[2..arg.len()]); - if let Some(opt) = opt { - if opt.1.is_some() { - // Expect a parameter for the given option - let opt_param = args.next().ok_or_else(|| { - format_err!("expected parameter for option {}", opt.0) - })?; - given.options.insert(opt.0, Some(opt_param)); - } else { - given.options.insert(opt.0, None); - } - } else { - bail!("invalid option"); - } - } else if let Some(name) = required.next() { - // First fill all required parameters - given.parameters.insert(name, arg); - } else if let Some(name) = optional.next() { - // Now fill all optional parameters - given.parameters.insert(name, arg); - } else { - bail!("to many arguments"); - } - } - // Check that we have got all required parameters - if required.next().is_some() { - bail!("not all required parameters provided"); - } - Ok((cmd, given)) - } -} - -/// Interpreted CLI arguments, stores parameters and options. -struct Args<'a> { - parameters: HashMap<&'static str, &'a [u8]>, - options: HashMap<&'static str, Option<&'a [u8]>>, -} - -impl<'a> Args<'a> { - /// Get a reference to the parameter give by name if present - fn get_param(&self, name: &str) -> Option<&&'a [u8]> { - self.parameters.get(name) - } - - /// Get a reference to the option give by name if present - fn get_opt(&self, name: &str) -> Option<&Option<&'a [u8]>> { - self.options.get(name) - } -} - -/// Definition of a shell command with its name, callback, description and -/// argument definition. -struct ShellCmd { - command: &'static str, - callback: fn(&mut ShellInstance, Args) -> Result<(), Error>, - description: &'static str, - options: Vec<(&'static str, Option<&'static str>)>, - required_parameters: Vec<&'static str>, - optional_parameters: Vec<&'static str>, -} - -impl ShellCmd { - /// Define a new `ShellCmd` with given command name, description and callback function. - fn new( - command: &'static str, - description: &'static str, - callback: fn(&mut ShellInstance, Args) -> Result<(), Error>, - ) -> Self { - Self { - command, - callback, - description, - options: Vec::new(), - required_parameters: Vec::new(), - optional_parameters: Vec::new(), - } - } - - /// Add additional named parameter `parameter` to command definition. - /// - /// The optional flag indicates if this parameter is required or optional. - fn parameter(mut self, parameter: &'static str, optional: bool) -> Self { - if optional { - self.optional_parameters.push(parameter); - } else { - self.required_parameters.push(parameter); - } - self - } - - /// Add additional named option `option` to command definition. - /// - /// The Option `parameter` indicates if this option has an additional parameter or not. - fn option(mut self, option: &'static str, parameter: Option<&'static str>) -> Self { - self.options.push((option, parameter)); - self - } - - /// Create the help String for this command - fn help(&self) -> String { - let mut help = String::new(); - help.push_str(self.command); - help.push_str("\n Usage:\t"); - help.push_str(self.command); - for opt in &self.options { - help.push_str(" [--"); - help.push_str(opt.0); - if let Some(opt_param) = opt.1 { - help.push(' '); - help.push_str(opt_param); - } - help.push(']'); - } - for par in &self.required_parameters { - help.push(' '); - help.push_str(par); - } - for par in &self.optional_parameters { - help.push_str(" ["); - help.push_str(par); - help.push(']'); - } - help.push_str("\n Description:\t"); - help.push_str(self.description); - help - } -} - -/// State of the shell instance -struct ShellInstance { - /// Readline context - rl: Readline, - /// List of paths selected for a restore - selected: HashSet>, - /// Decoder instance for the current pxar archive - decoder: Decoder, - /// Root directory for the give archive as stored in the catalog - root: Vec, -} - -impl ShellInstance { - /// Create a new `ShellInstance` for the given catalog and archive. pub fn new( mut catalog: CatalogReader, archive_name: &str, @@ -329,42 +35,181 @@ impl ShellInstance { let archive_root = catalog.lookup(&catalog_root, archive_name.as_bytes())?; let root = vec![archive_root]; - Ok(Self { - rl: Readline::new( - Self::generate_prompt(b"/"), - root.clone(), - Box::new(complete), + CONTEXT.with(|handle| { + let mut ctx = handle.borrow_mut(); + *ctx = Some(Context { catalog, - ), - selected: HashSet::new(), - decoder, - root, + selected: HashSet::new(), + decoder, + root: root.clone(), + current: root, + }); + }); + + // This list defines all the shell commands and their properties using the api schema + let cli_helper = CliHelper::new(CommandLineInterface::Nested( + CliCommandMap::new() + .insert("pwd", CliCommand::new(&API_METHOD_PWD_COMMAND).into()) + .insert( + "cd", + CliCommand::new(&API_METHOD_CD_COMMAND) + .arg_param(&["path"]) + .completion_cb("path", Self::complete_path) + .into(), + ) + .insert( + "ls", + CliCommand::new(&API_METHOD_LS_COMMAND) + .arg_param(&["path"]) + .completion_cb("path", Self::complete_path) + .into(), + ) + .insert( + "stat", + CliCommand::new(&API_METHOD_STAT_COMMAND) + .arg_param(&["path"]) + .completion_cb("path", Self::complete_path) + .into(), + ) + .insert( + "select", + CliCommand::new(&API_METHOD_SELECT_COMMAND) + .arg_param(&["path"]) + .completion_cb("path", Self::complete_path) + .into(), + ) + .insert( + "deselect", + CliCommand::new(&API_METHOD_DESELECT_COMMAND) + .arg_param(&["path"]) + .completion_cb("path", Self::complete_path) + .into(), + ) + .insert( + "restore-selected", + CliCommand::new(&API_METHOD_RESTORE_SELECTED_COMMAND) + .arg_param(&["target"]) + .completion_cb("target", tools::complete_file_name) + .into(), + ) + .insert( + "list-selected", + CliCommand::new(&API_METHOD_LIST_SELECTED_COMMAND).into(), + ) + .insert( + "restore", + CliCommand::new(&API_METHOD_RESTORE_COMMAND) + .arg_param(&["target"]) + .completion_cb("target", tools::complete_file_name) + .into(), + ) + .insert_help(), + )); + + let mut rl = rustyline::Editor::::new(); + rl.set_helper(Some(cli_helper)); + + Context::with(|ctx| { + Ok(Self { + rl, + prompt: ctx.generate_prompt()?, + }) }) } - /// Generate the CString to display by readline based on the - /// PROMPT_PREFIX, PROMPT_POST and the given byte slice. - fn generate_prompt(path: &[u8]) -> CString { - let mut buffer = Vec::new(); - buffer.extend_from_slice(PROMPT_PREFIX.as_bytes()); - buffer.extend_from_slice(path); - buffer.extend_from_slice(PROMPT_POST.as_bytes()); - unsafe { CString::from_vec_unchecked(buffer) } + /// Start the interactive shell loop + pub fn shell(mut self) -> Result<(), Error> { + while let Ok(line) = self.rl.readline(&self.prompt) { + let helper = self.rl.helper().unwrap(); + let args = match shellword_split(&line) { + Ok(args) => args, + Err(err) => { + println!("Error: {}", err); + continue; + } + }; + let _ = handle_command(helper.cmd_def(), "", args); + self.rl.add_history_entry(line); + self.update_prompt()?; + } + Ok(()) } - /// Get a mut ref to the context in order to be able to access the - /// catalog and the directory stack for the current working directory. - fn context(&mut self) -> &mut Context { - self.rl.context() + /// Update the prompt to the new working directory + fn update_prompt(&mut self) -> Result<(), Error> { + Context::with(|ctx| { + self.prompt = ctx.generate_prompt()?; + Ok(()) + }) } - /// Change the current working directory to the new directory - fn change_dir(&mut self, args: Args) -> Result<(), Error> { - let path = match args.get_param("path") { - Some(path) => *path, - None => &[], - }; - let mut path = self.canonical_path(path)?; + /// Completions for paths by lookup in the catalog + fn complete_path(complete_me: &str, _map: &HashMap) -> Vec { + Context::with(|ctx| { + let (base, to_complete) = match complete_me.rfind('/') { + // Split at ind + 1 so the slash remains on base, ok also if + // ends in slash as split_at accepts up to length as index. + Some(ind) => complete_me.split_at(ind + 1), + None => ("", complete_me), + }; + + let current = if base.is_empty() { + ctx.current.clone() + } else { + ctx.canonical_path(base)? + }; + + let entries = match ctx.catalog.read_dir(¤t.last().unwrap()) { + Ok(entries) => entries, + Err(_) => return Ok(Vec::new()), + }; + + let mut list = Vec::new(); + for entry in &entries { + let mut name = String::from(base); + if entry.name.starts_with(to_complete.as_bytes()) { + name.push_str(std::str::from_utf8(&entry.name)?); + if entry.is_directory() { + name.push('/'); + } + list.push(name); + } + } + Ok(list) + }) + .unwrap_or_default() + } +} + +#[api(input: { properties: {} })] +/// List the current working directory. +fn pwd_command() -> Result<(), Error> { + Context::with(|ctx| { + let path = Context::generate_cstring(&ctx.current)?; + let mut out = std::io::stdout(); + out.write_all(&path.as_bytes())?; + out.write_all(&[b'\n'])?; + out.flush()?; + Ok(()) + }) +} + +#[api( + input: { + properties: { + path: { + type: String, + optional: true, + description: "target path." + } + } + } +)] +/// Change the current working directory to the new directory +fn cd_command(path: Option) -> Result<(), Error> { + Context::with(|ctx| { + let path = path.unwrap_or_default(); + let mut path = ctx.canonical_path(&path)?; if !path .last() .ok_or_else(|| format_err!("invalid path component"))? @@ -374,54 +219,303 @@ impl ShellInstance { path.pop(); eprintln!("not a directory, fallback to parent directory"); } - self.context().current = path; - // Update the directory displayed in the prompt - let prompt = - Self::generate_prompt(Self::path(&self.context().current.clone())?.as_slice()); - self.rl.update_prompt(prompt); + ctx.current = path; Ok(()) - } + }) +} - /// List the content of a directory. - /// - /// Executed on files it returns the DirEntry of the file as single element - /// in the list. - fn list(&mut self, args: Args) -> Result<(), Error> { - let parent = if let Some(path) = args.get_param("path") { - // !path.is_empty() { - self.canonical_path(path)? +#[api( + input: { + properties: { + path: { + type: String, + optional: true, + description: "target path." + } + } + } +)] +/// List the content of working directory or given path. +fn ls_command(path: Option) -> Result<(), Error> { + Context::with(|ctx| { + let parent = if let Some(path) = path { + ctx.canonical_path(&path)? .last() .ok_or_else(|| format_err!("invalid path component"))? .clone() } else { - self.context().current.last().unwrap().clone() + ctx.current.last().unwrap().clone() }; let list = if parent.is_directory() { - self.context().catalog.read_dir(&parent)? + ctx.catalog.read_dir(&parent)? } else { vec![parent] }; - Self::print_list(&list).map_err(|err| format_err!("{}", err)) + + if list.is_empty() { + return Ok(()); + } + let max = list.iter().max_by(|x, y| x.name.len().cmp(&y.name.len())); + let max = match max { + Some(dir_entry) => dir_entry.name.len() + 1, + None => 0, + }; + + let (_rows, mut cols) = Context::get_terminal_size(); + cols /= max; + + let mut out = std::io::stdout(); + for (index, item) in list.iter().enumerate() { + out.write_all(&item.name)?; + // Fill with whitespaces + out.write_all(&vec![b' '; max - item.name.len()])?; + if index % cols == (cols - 1) { + out.write_all(&[b'\n'])?; + } + } + // If the last line is not complete, add the newline + if list.len() % cols != cols - 1 { + out.write_all(&[b'\n'])?; + } + out.flush()?; + Ok(()) + }) +} + +#[api( + input: { + properties: { + path: { + type: String, + description: "target path." + } + } + } +)] +/// Read the metadata for a given directory entry. +/// +/// This is expensive because the data has to be read from the pxar `Decoder`, +/// which means reading over the network. +fn stat_command(path: String) -> Result<(), Error> { + Context::with(|ctx| { + // First check if the file exists in the catalog, therefore avoiding + // expensive calls to the decoder just to find out that there maybe is + // no such entry. + // This is done by calling canonical_path(), which returns the full path + // if it exists, error otherwise. + let path = ctx.canonical_path(&path)?; + let (item, _attr, size) = ctx.lookup(&path)?; + let mut out = std::io::stdout(); + out.write_all(b"File: ")?; + out.write_all(item.filename.as_bytes())?; + out.write_all(&[b'\n'])?; + out.write_all(format!("Size: {}\n", size).as_bytes())?; + out.write_all(b"Type: ")?; + match item.entry.mode as u32 & libc::S_IFMT { + libc::S_IFDIR => out.write_all(b"directory\n")?, + libc::S_IFREG => out.write_all(b"regular file\n")?, + libc::S_IFLNK => out.write_all(b"symbolic link\n")?, + libc::S_IFBLK => out.write_all(b"block special file\n")?, + libc::S_IFCHR => out.write_all(b"character special file\n")?, + _ => out.write_all(b"unknown\n")?, + }; + out.write_all(format!("Uid: {}\n", item.entry.uid).as_bytes())?; + out.write_all(format!("Gid: {}\n", item.entry.gid).as_bytes())?; + out.flush()?; + Ok(()) + }) +} + +#[api( + input: { + properties: { + path: { + type: String, + description: "target path." + } + } + } +)] +/// Select an entry for restore. +/// +/// This will return an error if the entry is already present in the list or +/// if an invalid path was provided. +fn select_command(path: String) -> Result<(), Error> { + Context::with(|ctx| { + // Calling canonical_path() makes sure the provided path is valid and + // actually contained within the catalog and therefore also the archive. + let path = ctx.canonical_path(&path)?; + if ctx + .selected + .insert(Context::generate_cstring(&path)?.into_bytes()) + { + Ok(()) + } else { + bail!("entry already selected for restore") + } + }) +} + +#[api( + input: { + properties: { + path: { + type: String, + description: "path to entry to remove from list." + } + } + } +)] +/// Deselect an entry for restore. +/// +/// This will return an error if the entry was not found in the list of entries +/// selected for restore. +fn deselect_command(path: String) -> Result<(), Error> { + Context::with(|ctx| { + let path = ctx.canonical_path(&path)?; + if ctx.selected.remove(&Context::generate_cstring(&path)?.into_bytes()) { + Ok(()) + } else { + bail!("entry not selected for restore") + } + }) +} + +#[api( + input: { + properties: { + target: { + type: String, + description: "target path for restore on local filesystem." + } + } + } +)] +/// Restore the selected entries to the given target path. +/// +/// Target must not exist on the clients filesystem. +fn restore_selected_command(target: String) -> Result<(), Error> { + Context::with(|ctx| { + let mut list = Vec::new(); + for path in &ctx.selected { + let pattern = MatchPattern::from_line(path)? + .ok_or_else(|| format_err!("encountered invalid match pattern"))?; + list.push(pattern); + } + if list.is_empty() { + bail!("no entries selected for restore"); + } + + // Entry point for the restore is always root here as the provided match + // patterns are relative to root as well. + let start_dir = ctx.decoder.root()?; + ctx.decoder + .restore(&start_dir, &Path::new(&target), &list)?; + Ok(()) + }) +} + +#[api( input: { properties: {} })] +/// List entries currently selected for restore. +fn list_selected_command() -> Result<(), Error> { + Context::with(|ctx| { + let mut out = std::io::stdout(); + for entry in &ctx.selected { + out.write_all(entry)?; + out.write_all(&[b'\n'])?; + } + out.flush()?; + Ok(()) + }) +} + +#[api( + input: { + properties: { + target: { + type: String, + description: "target path for restore on local filesystem." + }, + pattern: { + type: String, + optional: true, + description: "match pattern to limit files for restore." + } + } + } +)] +/// Restore the sub-archive given by the current working directory to target. +/// +/// By further providing a pattern, the restore can be limited to a narrower +/// subset of this sub-archive. +/// If pattern is not present or empty, the full archive is restored to target. +fn restore_command(target: String, pattern: Option) -> Result<(), Error> { + Context::with(|ctx| { + let pattern = pattern.unwrap_or_default(); + let match_pattern = match pattern.as_str() { + "" | "/" | "." => Vec::new(), + _ => vec![MatchPattern::from_line(pattern.as_bytes())?.unwrap()], + }; + // Decoder entry point for the restore. + let start_dir = if pattern.starts_with("/") { + ctx.decoder.root()? + } else { + // Get the directory corresponding to the working directory from the + // archive. + let cwd = ctx.current.clone(); + let (dir, _, _) = ctx.lookup(&cwd)?; + dir + }; + + ctx.decoder + .restore(&start_dir, &Path::new(&target), &match_pattern)?; + Ok(()) + }) +} + +std::thread_local! { + static CONTEXT: RefCell> = RefCell::new(None); +} + +/// Holds the context needed for access to catalog and decoder +struct Context { + /// Calalog reader instance to navigate + catalog: CatalogReader, + /// List of selected paths for restore + selected: HashSet>, + /// Decoder instance for the current pxar archive + decoder: Decoder, + /// Root directory for the give archive as stored in the catalog + root: Vec, + /// Stack of directories up to the current working directory + /// used for navigation and path completion. + current: Vec, +} + +impl Context { + /// Execute `call` within a context providing a mut ref to `Context` instance. + fn with(call: F) -> Result + where + F: FnOnce(&mut Context) -> Result, + { + CONTEXT.with(|cell| { + let mut ctx = cell.borrow_mut(); + call(&mut ctx.as_mut().unwrap()) + }) } - /// Print the current working directory - fn pwd(&mut self, _args: Args) -> Result<(), Error> { - let pwd = Self::path(&self.context().current.clone())?; - Self::print_slice(&pwd).map_err(|err| format_err!("{}", err)) - } - - /// Generate an absolute path from a directory stack. - fn path(dir_stack: &[DirEntry]) -> Result, Error> { + /// Generate CString from provided stack of `DirEntry`s. + fn generate_cstring(dir_stack: &[DirEntry]) -> Result { let mut path = vec![b'/']; - // Skip the archive root, '/' is displayed for it - for item in dir_stack.iter().skip(1) { - path.extend_from_slice(&item.name); - if item.is_directory() { + // Skip the archive root, the '/' is displayed for it instead + for component in dir_stack.iter().skip(1) { + path.extend_from_slice(&component.name); + if component.is_directory() { path.push(b'/'); } } - Ok(path) + Ok(unsafe { CString::from_vec_unchecked(path) }) } /// Resolve the indirect path components and return an absolute path. @@ -430,8 +524,8 @@ impl ShellInstance { /// path is vaild and exists. /// This does not include following symbolic links. /// If None is given as path, only the root directory is returned. - fn canonical_path(&mut self, path: &[u8]) -> Result, Error> { - if path == b"/" { + fn canonical_path(&mut self, path: &str) -> Result, Error> { + if path == "/" { return Ok(self.root.clone()); } @@ -442,34 +536,31 @@ impl ShellInstance { path }; - let mut dir_stack = if path_slice.starts_with(&[b'/']) { + let mut dir_stack = if path_slice.starts_with("/") { // Absolute path, reduce view of slice and start from root path_slice = &path_slice[1..]; self.root.clone() } else { // Relative path, start from current working directory - self.context().current.clone() + self.current.clone() }; - let should_end_dir = if path_slice.ends_with(&[b'/']) { + let should_end_dir = if path_slice.ends_with("/") { path_slice = &path_slice[0..path_slice.len() - 1]; true } else { false }; - for name in path_slice.split(|b| *b == b'/') { + for name in path_slice.split('/') { match name { - b"." => continue, - b".." => { + "." => continue, + ".." => { // Never pop archive root from stack if dir_stack.len() > 1 { dir_stack.pop(); } } _ => { - let entry = self - .context() - .catalog - .lookup(dir_stack.last().unwrap(), name)?; + let entry = self.catalog.lookup(dir_stack.last().unwrap(), name.as_bytes())?; dir_stack.push(entry); } } @@ -486,221 +577,16 @@ impl ShellInstance { Ok(dir_stack) } - /// Read the metadata for a given directory entry. - /// - /// This is expensive because the data has to be read from the pxar `Decoder`, - /// which means reading over the network. - fn stat(&mut self, args: Args) -> Result<(), Error> { - let path = args - .get_param("path") - .ok_or_else(|| format_err!("no path provided"))?; - // First check if the file exists in the catalog, therefore avoiding - // expensive calls to the decoder just to find out that there could be no - // such entry. This is done by calling canonical_path(), which returns - // the full path if it exists, error otherwise. - let path = self.canonical_path(path)?; - let (entry, attr, size) = self.lookup(&path)?; - Self::print_stat(&entry, &attr, size).map_err(|err| format_err!("{}", err)) - } - - /// Look up the entry given by a canonical absolute `path` in the archive. - /// - /// This will actively navigate the archive by calling the corresponding decoder - /// functionalities and is therefore very expensive. - fn lookup( - &mut self, - absolute_path: &[DirEntry], - ) -> Result<(DirectoryEntry, PxarAttributes, u64), Error> { - let mut current = self.decoder.root()?; - let (_, _, mut attr, mut size) = self.decoder.attributes(0)?; - // Ignore the archive root, don't need it. - for item in absolute_path.iter().skip(1) { - match self - .decoder - .lookup(¤t, &OsStr::from_bytes(&item.name))? - { - Some((item, item_attr, item_size)) => { - current = item; - attr = item_attr; - size = item_size; - } - // This should not happen if catalog an archive are consistent. - None => bail!("no such file or directory in archive"), - } - } - Ok((current, attr, size)) - } - - /// Select an entry for restore. - /// - /// This will return an error if the entry is already present in the list or - /// if an invalid path was provided. - fn select(&mut self, args: Args) -> Result<(), Error> { - let path = args - .get_param("path") - .ok_or_else(|| format_err!("no path provided"))?; - // Calling canonical_path() makes sure the provided path is valid and - // actually contained within the catalog and therefore also the archive. - let path = self.canonical_path(path)?; - if self.selected.insert(Self::path(&path)?) { - Ok(()) - } else { - bail!("entry already selected for restore") - } - } - - /// Deselect an entry for restore. - /// - /// This will return an error if the entry was not found in the list of entries - /// selected for restore. - fn deselect(&mut self, args: Args) -> Result<(), Error> { - let path = args - .get_param("path") - .ok_or_else(|| format_err!("no path provided"))?; - if self.selected.remove(*path) { - Ok(()) - } else { - bail!("entry not selected for restore") - } - } - - /// Restore the selected entries to the given target path. - /// - /// Target must not exist on the clients filesystem. - fn restore_selected(&mut self, args: Args) -> Result<(), Error> { - let target = args - .get_param("target") - .ok_or_else(|| format_err!("no target provided"))?; - let mut list = Vec::new(); - for path in &self.selected { - let pattern = MatchPattern::from_line(path)? - .ok_or_else(|| format_err!("encountered invalid match pattern"))?; - list.push(pattern); - } - if list.is_empty() { - bail!("no entries selected for restore"); - } - - // Entry point for the restore is always root here as the provided match - // patterns are relative to root as well. - let start_dir = self.decoder.root()?; - let target: &OsStr = OsStrExt::from_bytes(target); - self.decoder - .restore(&start_dir, &Path::new(target), &list)?; - Ok(()) - } - - /// List entries currently selected for restore. - fn list_selected(&mut self, _args: Args) -> Result<(), Error> { - let mut out = std::io::stdout(); - for entry in &self.selected { - out.write_all(entry).map_err(|err| format_err!("{}", err))?; - out.write_all(&[b'\n']) - .map_err(|err| format_err!("{}", err))?; - } - out.flush().map_err(|err| format_err!("{}", err))?; - Ok(()) - } - - /// Restore the sub-archive given by the current working directory to target. - /// - /// By further providing a pattern, the restore can be limited to a narrower - /// subset of this sub-archive. - /// If pattern is an empty slice, the full dir is restored. - fn restore(&mut self, args: Args) -> Result<(), Error> { - let target = args.get_param("target").unwrap(); - let pattern = args.get_opt("pattern").unwrap().unwrap_or(&[]); - let match_pattern = match pattern { - b"" | b"/" | b"." => Vec::new(), - _ => vec![MatchPattern::from_line(pattern)?.unwrap()], - }; - // Entry point for the restore. - let start_dir = if pattern.starts_with(&[b'/']) { - self.decoder.root()? - } else { - // Get the directory corresponding to the working directory from the - // archive. - let cwd = self.context().current.clone(); - let (dir, _, _) = self.lookup(&cwd)?; - dir - }; - - let target: &OsStr = OsStrExt::from_bytes(target); - self.decoder - .restore(&start_dir, &Path::new(target), &match_pattern)?; - Ok(()) - } - - /// Dummy callback for the help command. - fn help(&mut self, _args: Args) -> Result<(), Error> { - // this is a dummy, the actual help is handled before calling the callback - // as the full set of available commands is needed. - Ok(()) - } - - /// Print the list of `DirEntry`s to stdout. - fn print_list(list: &[DirEntry]) -> Result<(), std::io::Error> { - if list.is_empty() { - return Ok(()); - } - let max = list.iter().max_by(|x, y| x.name.len().cmp(&y.name.len())); - let max = match max { - Some(dir_entry) => dir_entry.name.len() + 1, - None => 0, - }; - let (_rows, mut cols) = Self::get_terminal_size(); - cols /= max; - let mut out = std::io::stdout(); - - for (index, item) in list.iter().enumerate() { - out.write_all(&item.name)?; - // Fill with whitespaces - out.write_all(&vec![b' '; max - item.name.len()])?; - if index % cols == (cols - 1) { - out.write_all(&[b'\n'])?; - } - } - // If the last line is not complete, add the newline - if list.len() % cols != cols - 1 { - out.write_all(&[b'\n'])?; - } - out.flush()?; - Ok(()) - } - - /// Print the given byte slice to stdout. - fn print_slice(slice: &[u8]) -> Result<(), std::io::Error> { - let mut out = std::io::stdout(); - out.write_all(slice)?; - out.write_all(&[b'\n'])?; - out.flush()?; - Ok(()) - } - - /// Print the stats of `DirEntry` item to stdout. - fn print_stat( - item: &DirectoryEntry, - _attr: &PxarAttributes, - size: u64, - ) -> Result<(), std::io::Error> { - let mut out = std::io::stdout(); - out.write_all(b"File: ")?; - out.write_all(&item.filename.as_bytes())?; - out.write_all(&[b'\n'])?; - out.write_all(format!("Size: {}\n", size).as_bytes())?; - out.write_all(b"Type: ")?; - match item.entry.mode as u32 & libc::S_IFMT { - libc::S_IFDIR => out.write_all(b"directory\n")?, - libc::S_IFREG => out.write_all(b"regular file\n")?, - libc::S_IFLNK => out.write_all(b"symbolic link\n")?, - libc::S_IFBLK => out.write_all(b"block special file\n")?, - libc::S_IFCHR => out.write_all(b"character special file\n")?, - _ => out.write_all(b"unknown\n")?, - }; - out.write_all(format!("Uid: {}\n", item.entry.uid).as_bytes())?; - out.write_all(format!("Gid: {}\n", item.entry.gid).as_bytes())?; - out.flush()?; - Ok(()) + /// Generate the CString to display by readline based on + /// PROMPT_PREFIX, PROMPT and the current working directory. + fn generate_prompt(&self) -> Result { + let prompt = format!( + "{}{} {} ", + PROMPT_PREFIX, + Self::generate_cstring(&self.current)?.to_string_lossy(), + PROMPT, + ); + Ok(prompt) } /// Get the current size of the terminal @@ -727,56 +613,32 @@ impl ShellInstance { unsafe { libc::ioctl(libc::STDOUT_FILENO, TIOCGWINSZ, &mut winsize) }; (winsize.ws_row as usize, winsize.ws_col as usize) } -} -/// Filename completion callback for the shell -// TODO: impl command completion. For now only filename completion. -fn complete(ctx: &mut Context, text: &CStr, _start: usize, _end: usize) -> Vec { - let slices: Vec<_> = text.to_bytes().split(|b| *b == b'/').collect(); - let to_complete = match slices.last() { - Some(last) => last, - None => return Vec::new(), - }; - let mut current = ctx.current.clone(); - let (prefix, entries) = { - let mut prefix = Vec::new(); - if slices.len() > 1 { - for component in &slices[..slices.len() - 1] { - if component == b"." { - continue; - } else if component == b".." { - // Never leave the current archive in the catalog - if current.len() > 1 { - current.pop(); - } - } else { - match ctx.catalog.lookup(current.last().unwrap(), component) { - Err(_) => return Vec::new(), - Ok(dir) => current.push(dir), - } + /// Look up the entry given by a canonical absolute `path` in the archive. + /// + /// This will actively navigate the archive by calling the corresponding + /// decoder functionalities and is therefore very expensive. + fn lookup( + &mut self, + absolute_path: &[DirEntry], + ) -> Result<(DirectoryEntry, PxarAttributes, u64), Error> { + let mut current = self.decoder.root()?; + let (_, _, mut attr, mut size) = self.decoder.attributes(0)?; + // Ignore the archive root, don't need it. + for item in absolute_path.iter().skip(1) { + match self + .decoder + .lookup(¤t, &OsStr::from_bytes(&item.name))? + { + Some((item, item_attr, item_size)) => { + current = item; + attr = item_attr; + size = item_size; } - prefix.extend_from_slice(component); - prefix.push(b'/'); + // This should not happen if catalog an archive are consistent. + None => bail!("no such file or directory in archive - inconsistent catalog"), } } - let entries = match ctx.catalog.read_dir(¤t.last().unwrap()) { - Ok(entries) => entries, - Err(_) => return Vec::new(), - }; - (prefix, entries) - }; - // Create a list of completion strings which outlives this function - let mut list = Vec::new(); - for entry in &entries { - if entry.name.starts_with(to_complete) { - let mut name_buf = prefix.clone(); - name_buf.extend_from_slice(&entry.name); - if entry.is_directory() { - name_buf.push(b'/'); - } - let name = unsafe { CString::from_vec_unchecked(name_buf) }; - list.push(name); - } + Ok((current, attr, size)) } - list }