-use std::collections::HashSet;
-use std::ffi::{CStr, CString, OsStr};
+use std::cell::RefCell;
+use std::collections::{HashMap, HashSet};
+use std::ffi::{CString, OsStr};
use std::io::Write;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use failure::*;
-use libc;
+use super::catalog::{CatalogReader, DirEntry};
use crate::pxar::*;
+use crate::tools;
-use super::catalog::{CatalogReader, DirEntry};
-use super::readline::{Readline, Context};
+use proxmox::api::{cli::*, *};
-/// State of the shell instance
+const PROMPT_PREFIX: &str = "pxar:";
+const PROMPT: &str = ">";
+
+/// Interactive shell for interacton with the catalog.
pub struct Shell {
- /// Readline context
- rl: Readline,
- /// List of paths selected for a restore
- selected: HashSet<Vec<u8>>,
- /// Decoder instance for the current pxar archive
- decoder: Decoder,
- /// Root directory for the give archive as stored in the catalog
- root: Vec<DirEntry>,
+ /// Readline instance handling input and callbacks
+ rl: rustyline::Editor<CliHelper>,
+ prompt: String,
}
-/// All supported commands of the shell
-enum Command<'a> {
- /// List the content of the current dir or in path, if provided
- List(&'a [u8]),
- /// Stat of the provided path
- Stat(&'a [u8]),
- /// Select the given entry for a restore
- Select(&'a [u8]),
- /// Remove the entry from the list of entries to restore
- Deselect(&'a [u8]),
- /// Restore an archive to the provided target, can be limited to files
- /// matching the provided match pattern
- Restore(&'a [u8], &'a [u8]),
- /// Restore the selected entries to the provided target
- RestoreSelected(&'a [u8]),
- /// List the entries currently selected for restore
- ListSelected,
- /// Change the current working directory
- ChangeDir(&'a [u8]),
- /// Print the working directory
- PrintWorkingDir,
- /// Terminate the shell loop, returns from the shell
- Quit,
- /// Empty line from readline
- Empty,
+/// This list defines all the shell commands and their properties
+/// using the api schema
+pub fn catalog_shell_cli() -> CommandLineInterface {
+
+ let map = CliCommandMap::new()
+ .insert("pwd", CliCommand::new(&API_METHOD_PWD_COMMAND))
+ .insert(
+ "cd",
+ CliCommand::new(&API_METHOD_CD_COMMAND)
+ .arg_param(&["path"])
+ .completion_cb("path", Shell::complete_path)
+ )
+ .insert(
+ "ls",
+ CliCommand::new(&API_METHOD_LS_COMMAND)
+ .arg_param(&["path"])
+ .completion_cb("path", Shell::complete_path)
+ )
+ .insert(
+ "stat",
+ CliCommand::new(&API_METHOD_STAT_COMMAND)
+ .arg_param(&["path"])
+ .completion_cb("path", Shell::complete_path)
+ )
+ .insert(
+ "select",
+ CliCommand::new(&API_METHOD_SELECT_COMMAND)
+ .arg_param(&["path"])
+ .completion_cb("path", Shell::complete_path)
+ )
+ .insert(
+ "deselect",
+ CliCommand::new(&API_METHOD_DESELECT_COMMAND)
+ .arg_param(&["path"])
+ .completion_cb("path", Shell::complete_path)
+ )
+ .insert(
+ "restore-selected",
+ CliCommand::new(&API_METHOD_RESTORE_SELECTED_COMMAND)
+ .arg_param(&["target"])
+ .completion_cb("target", tools::complete_file_name)
+ )
+ .insert(
+ "list-selected",
+ CliCommand::new(&API_METHOD_LIST_SELECTED_COMMAND),
+ )
+ .insert(
+ "restore",
+ CliCommand::new(&API_METHOD_RESTORE_COMMAND)
+ .arg_param(&["target"])
+ .completion_cb("target", tools::complete_file_name)
+ )
+ .insert_help();
+
+ CommandLineInterface::Nested(map)
}
-const PROMPT_PREFIX: &str = "pxar:";
-const PROMPT_POST: &str = " > ";
-
impl Shell {
+ /// Create a new shell for the given catalog and pxar archive.
pub fn new(
mut catalog: CatalogReader<std::fs::File>,
archive_name: &str,
// The root for the given archive as stored in the catalog
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,
+ });
+ });
+
+ let cli_helper = CliHelper::new(catalog_shell_cli());
+ let mut rl = rustyline::Editor::<CliHelper>::new();
+ rl.set_helper(Some(cli_helper));
+
+ Context::with(|ctx| {
+ Ok(Self {
+ rl,
+ prompt: ctx.generate_prompt()?,
+ })
})
}
- 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 Some(line) = self.rl.readline() {
- let res = match self.parse_command(&line) {
- Ok(Command::List(path)) => self.list(path).and_then(|list| {
- Self::print_list(&list).map_err(|err| format_err!("{}", err))?;
- Ok(())
- }),
- Ok(Command::ChangeDir(path)) => self.change_dir(path),
- Ok(Command::Restore(target, pattern)) => self.restore(target, pattern),
- Ok(Command::Select(path)) => self.select(path),
- Ok(Command::Deselect(path)) => self.deselect(path),
- Ok(Command::RestoreSelected(target)) => self.restore_selected(target),
- Ok(Command::Stat(path)) => self.stat(&path).and_then(|(item, attr, size)| {
- Self::print_stat(&item, &attr, size)?;
- Ok(())
- }),
- Ok(Command::ListSelected) => {
- self.list_selected().map_err(|err| format_err!("{}", err))
+ 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;
}
- Ok(Command::PrintWorkingDir) => self.pwd().and_then(|pwd| {
- Self::print_pwd(&pwd).map_err(|err| format_err!("{}", err))?;
- Ok(())
- }),
- Ok(Command::Quit) => break,
- Ok(Command::Empty) => continue,
- Err(err) => Err(err),
};
- if let Err(err) = res {
- println!("error: {}", err);
- }
+ let _ = handle_command(helper.cmd_def(), "", args);
+ self.rl.add_history_entry(line);
+ self.update_prompt()?;
}
Ok(())
}
- /// Command parser mapping the line returned by readline to a command.
- fn parse_command<'a>(&self, line: &'a [u8]) -> Result<Command<'a>, 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();
+ /// Update the prompt to the new working directory
+ fn update_prompt(&mut self) -> Result<(), Error> {
+ Context::with(|ctx| {
+ self.prompt = ctx.generate_prompt()?;
+ Ok(())
+ })
+ }
+
+ /// Completions for paths by lookup in the catalog
+ fn complete_path(complete_me: &str, _map: &HashMap<String, String>) -> Vec<String> {
+ 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),
+ };
- if args.is_empty() {
- return Ok(Command::Empty);
- }
+ let current = if base.is_empty() {
+ ctx.current.clone()
+ } else {
+ ctx.canonical_path(base)?
+ };
- match args[0] {
- b"quit" => Ok(Command::Quit),
- b"exit" => Ok(Command::Quit),
- b"ls" => match args.len() {
- 1 => Ok(Command::List(&[])),
- 2 => Ok(Command::List(args[1])),
- _ => bail!("To many parameters!"),
- },
- b"pwd" => Ok(Command::PrintWorkingDir),
- b"restore" => match args.len() {
- 1 => bail!("no target provided"),
- 2 => Ok(Command::Restore(args[1], &[])),
- 4 => if args[2] == b"-p" {
- Ok(Command::Restore(args[1], args[3]))
- } else {
- bail!("invalid parameter")
+ 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);
}
- _ => bail!("to many parameters"),
- },
- b"cd" => match args.len() {
- 1 => Ok(Command::ChangeDir(&[])),
- 2 => Ok(Command::ChangeDir(args[1])),
- _ => bail!("to many parameters"),
- },
- b"stat" => match args.len() {
- 1 => bail!("no path provided"),
- 2 => Ok(Command::Stat(args[1])),
- _ => bail!("to many parameters"),
- },
- b"select" => match args.len() {
- 1 => bail!("no path provided"),
- 2 => Ok(Command::Select(args[1])),
- _ => bail!("to many parameters"),
- },
- b"deselect" => match args.len() {
- 1 => bail!("no path provided"),
- 2 => Ok(Command::Deselect(args[1])),
- _ => bail!("to many parameters"),
- },
- b"selected" => match args.len() {
- 1 => Ok(Command::ListSelected),
- _ => bail!("to many parameters"),
- },
- b"restore-selected" => match args.len() {
- 1 => bail!("no path provided"),
- 2 => Ok(Command::RestoreSelected(args[1])),
- _ => bail!("to many parameters"),
- },
- _ => bail!("command not known"),
- }
+ }
+ Ok(list)
+ })
+ .unwrap_or_default()
}
+}
- /// 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()
- }
+#[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(())
+ })
+}
- /// Change the current working directory to the new directory
- fn change_dir(&mut self, path: &[u8]) -> Result<(), Error> {
- let mut path = self.canonical_path(path)?;
+#[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<String>) -> 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"))?
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.pwd()?.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, path: &[u8]) -> Result<Vec<DirEntry>, Error> {
- let parent = if !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<String>) -> 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]
};
- Ok(list)
- }
- /// Return the current working directory as string
- fn pwd(&mut self) -> Result<Vec<u8>, Error> {
- Self::to_path(&self.context().current.clone())
- }
-
- /// Generate an absolute path from a directory stack.
- fn to_path(dir_stack: &[DirEntry]) -> Result<Vec<u8>, Error> {
- 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() {
- path.push(b'/');
- }
- }
- Ok(path)
- }
-
- /// Resolve the indirect path components and return an absolute path.
- ///
- /// This will actually navigate the filesystem tree to check that the
- /// 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<Vec<DirEntry>, Error> {
- if path == b"/" {
- return Ok(self.root.clone());
+ if list.is_empty() {
+ return Ok(());
}
-
- let mut path_slice = if path.is_empty() {
- // Fallback to root if no path was provided
- return Ok(self.root.clone());
- } else {
- path
+ 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 mut dir_stack = if path_slice.starts_with(&[b'/']) {
- // 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()
- };
- let should_end_dir = if path_slice.ends_with(&[b'/']) {
- path_slice = &path_slice[0..path_slice.len() - 1];
- true
- } else {
- false
- };
- for name in path_slice.split(|b| *b == b'/') {
- match name {
- b"." => continue,
- b".." => {
- // 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)?;
- dir_stack.push(entry);
- }
+ 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 should_end_dir && !dir_stack.last()
- .ok_or_else(|| format_err!("invalid path component"))?
- .is_directory()
- {
- bail!("entry is not a directory");
+ // If the last line is not complete, add the newline
+ if list.len() % cols != cols - 1 {
+ out.write_all(&[b'\n'])?;
}
+ out.flush()?;
+ Ok(())
+ })
+}
- Ok(dir_stack)
+#[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(&mut self, path: &[u8]) -> Result<(DirectoryEntry, PxarAttributes, u64), Error> {
+)]
+/// 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 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)?;
- self.lookup(&path)
- }
+ // 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(())
+ })
+}
- /// 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"),
+#[api(
+ input: {
+ properties: {
+ path: {
+ type: String,
+ description: "target path."
}
}
- 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, path: &[u8]) -> Result<(), Error> {
+)]
+/// 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 = self.canonical_path(path)?;
- if self.selected.insert(Self::to_path(&path)?) {
+ let path = ctx.canonical_path(&path)?;
+ if ctx
+ .selected
+ .insert(Context::generate_cstring(&path)?.into_bytes())
+ {
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, path: &[u8]) -> Result<(), Error> {
- if self.selected.remove(path) {
+#[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")
}
- }
+ })
+}
- /// Restore the selected entries to the given target path.
- ///
- /// Target must not exist on the clients filesystem.
- fn restore_selected(&mut self, target: &[u8]) -> Result<(), Error> {
+#[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 &self.selected {
+ for path in &ctx.selected {
let pattern = MatchPattern::from_line(path)?
.ok_or_else(|| format_err!("encountered invalid match pattern"))?;
list.push(pattern);
// 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)?;
+ let start_dir = ctx.decoder.root()?;
+ ctx.decoder
+ .restore(&start_dir, &Path::new(&target), &list)?;
Ok(())
- }
+ })
+}
- /// List entries currently selected for restore.
- fn list_selected(&self) -> Result<(), std::io::Error> {
+#[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 &self.selected {
+ for entry in &ctx.selected {
out.write_all(entry)?;
out.write_all(&[b'\n'])?;
}
out.flush()?;
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, target: &[u8], pattern: &[u8]) -> Result<(), Error> {
- let match_pattern = match pattern {
- b"" | b"/" | b"." => Vec::new(),
- _ => vec![MatchPattern::from_line(pattern)?.unwrap()],
+#[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<String>) -> 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()],
};
- // Entry point for the restore.
- let start_dir = if pattern.starts_with(&[b'/']) {
- self.decoder.root()?
+ // 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 = self.context().current.clone();
- let (dir, _, _) = self.lookup(&cwd)?;
+ let cwd = ctx.current.clone();
+ let (dir, _, _) = ctx.lookup(&cwd)?;
dir
};
- let target: &OsStr = OsStrExt::from_bytes(target);
- self.decoder.restore(&start_dir, &Path::new(target), &match_pattern)?;
+ ctx.decoder
+ .restore(&start_dir, &Path::new(&target), &match_pattern)?;
Ok(())
+ })
+}
+
+std::thread_local! {
+ static CONTEXT: RefCell<Option<Context>> = RefCell::new(None);
+}
+
+/// Holds the context needed for access to catalog and decoder
+struct Context {
+ /// Calalog reader instance to navigate
+ catalog: CatalogReader<std::fs::File>,
+ /// List of selected paths for restore
+ selected: HashSet<Vec<u8>>,
+ /// Decoder instance for the current pxar archive
+ decoder: Decoder,
+ /// Root directory for the give archive as stored in the catalog
+ root: Vec<DirEntry>,
+ /// Stack of directories up to the current working directory
+ /// used for navigation and path completion.
+ current: Vec<DirEntry>,
+}
+
+impl Context {
+ /// Execute `call` within a context providing a mut ref to `Context` instance.
+ fn with<T, F>(call: F) -> Result<T, Error>
+ where
+ F: FnOnce(&mut Context) -> Result<T, Error>,
+ {
+ CONTEXT.with(|cell| {
+ let mut ctx = cell.borrow_mut();
+ call(&mut ctx.as_mut().unwrap())
+ })
}
- fn print_list(list: &Vec<DirEntry>) -> Result<(), std::io::Error> {
- 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,
+ /// Generate CString from provided stack of `DirEntry`s.
+ fn generate_cstring(dir_stack: &[DirEntry]) -> Result<CString, Error> {
+ let mut path = vec![b'/'];
+ // 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(unsafe { CString::from_vec_unchecked(path) })
+ }
+
+ /// Resolve the indirect path components and return an absolute path.
+ ///
+ /// This will actually navigate the filesystem tree to check that the
+ /// 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: &str) -> Result<Vec<DirEntry>, Error> {
+ if path == "/" {
+ return Ok(self.root.clone());
+ }
+
+ let mut path_slice = if path.is_empty() {
+ // Fallback to root if no path was provided
+ return Ok(self.root.clone());
+ } else {
+ path
};
- 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'])?;
+ 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.current.clone()
+ };
+ 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('/') {
+ match name {
+ "." => continue,
+ ".." => {
+ // Never pop archive root from stack
+ if dir_stack.len() > 1 {
+ dir_stack.pop();
+ }
+ }
+ _ => {
+ let entry = self.catalog.lookup(dir_stack.last().unwrap(), name.as_bytes())?;
+ dir_stack.push(entry);
+ }
}
}
- // If the last line is not complete, add the newline
- if list.len() % cols != cols - 1 {
- out.write_all(&[b'\n'])?;
+ if should_end_dir
+ && !dir_stack
+ .last()
+ .ok_or_else(|| format_err!("invalid path component"))?
+ .is_directory()
+ {
+ bail!("entry is not a directory");
}
- out.flush()?;
- Ok(())
- }
- fn print_pwd(pwd: &[u8]) -> Result<(), std::io::Error> {
- let mut out = std::io::stdout();
- out.write_all(pwd)?;
- out.write_all(&[b'\n'])?;
- out.flush()?;
- Ok(())
+ Ok(dir_stack)
}
- fn print_stat(item: &DirectoryEntry, _attr: &PxarAttributes, size: u64) -> Result<(), std::io::Error> {
- let mut out = std::io::stdout();
- out.write_all("File: ".as_bytes())?;
- out.write_all(&item.filename.as_bytes())?;
- out.write_all(&[b'\n'])?;
- out.write_all(format!("Size: {}\n", size).as_bytes())?;
- let mode = match item.entry.mode as u32 & libc::S_IFMT {
- libc::S_IFDIR => "directory".as_bytes(),
- libc::S_IFREG => "regular file".as_bytes(),
- libc::S_IFLNK => "symbolic link".as_bytes(),
- libc::S_IFBLK => "block special file".as_bytes(),
- libc::S_IFCHR => "character special file".as_bytes(),
- _ => "unknown".as_bytes(),
- };
- out.write_all("Type: ".as_bytes())?;
- out.write_all(&mode)?;
- out.write_all(&[b'\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<String, Error> {
+ let prompt = format!(
+ "{}{} {} ",
+ PROMPT_PREFIX,
+ Self::generate_cstring(&self.current)?.to_string_lossy(),
+ PROMPT,
+ );
+ Ok(prompt)
}
/// Get the current size of the terminal
+ /// # Safety
///
- /// uses tty_ioctl, see man tty_ioctl(2)
+ /// uses unsafe call to tty_ioctl, see man tty_ioctl(2)
fn get_terminal_size() -> (usize, usize) {
-
- const TIOCGWINSZ: libc::c_ulong = 0x00005413;
+ const TIOCGWINSZ: libc::c_ulong = 0x5413;
#[repr(C)]
struct WinSize {
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<CString> {
- 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'/');
- }
- }
- 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'/');
+ // This should not happen if catalog an archive are consistent.
+ None => bail!("no such file or directory in archive - inconsistent catalog"),
}
- let name = unsafe { CString::from_vec_unchecked(name_buf) };
- list.push(name);
}
+ Ok((current, attr, size))
}
- list
}